Some checks failed
Security Monitoring & Compliance / Static Code Security Analysis (push) Has been cancelled
Security Monitoring & Compliance / Security Compliance Validation (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Security Analysis (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Code Quality & Standards (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Unit Tests (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Integration Tests (push) Has been cancelled
Security Monitoring & Compliance / Dependency Vulnerability Scan (push) Has been cancelled
Security Monitoring & Compliance / Secrets & Credential Scan (push) Has been cancelled
Security Monitoring & Compliance / WordPress Security Analysis (push) Has been cancelled
Security Monitoring & Compliance / Security Summary Report (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Deploy to Production (push) Has been cancelled
HVAC Plugin CI/CD Pipeline / Notification (push) Has been cancelled
Security Monitoring & Compliance / Security Team Notification (push) Has been cancelled
Major modernization of HVAC plugin for PHP 8+ with full backward compatibility: CORE MODERNIZATION: - Implement strict type declarations throughout codebase - Modernize main plugin class with PHP 8+ features - Convert array syntax to modern PHP format - Add constructor property promotion where applicable - Enhance security helpers with modern PHP patterns COMPATIBILITY FIXES: - Fix PHP 8.1+ enum compatibility (convert to class constants) - Fix union type compatibility (true|WP_Error → bool|WP_Error) - Remove mixed type declarations for PHP 8.0 compatibility - Add default arms to match expressions preventing UnhandledMatchError - Fix method naming inconsistency (ensureRegistrationAccess callback) - Add null coalescing in TEC integration for strict type compliance DEPLOYMENT STATUS: ✅ Successfully deployed and tested on staging ✅ Site functional at https://upskill-staging.measurequick.com ✅ Expert code review completed with GPT-5 validation ✅ MCP Playwright testing confirms functionality Ready for production deployment when requested. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
644 lines
No EOL
21 KiB
PHP
644 lines
No EOL
21 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* HVAC Security Helpers
|
|
*
|
|
* Centralized security functions for the HVAC Community Events plugin
|
|
*
|
|
* @package HVAC_Community_Events
|
|
* @since 2.1.0
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Superglobal types constants for enhanced security
|
|
*/
|
|
final class SuperglobalType
|
|
{
|
|
public const GET = 'GET';
|
|
public const POST = 'POST';
|
|
public const REQUEST = 'REQUEST';
|
|
public const COOKIE = 'COOKIE';
|
|
public const SERVER = 'SERVER';
|
|
}
|
|
|
|
/**
|
|
* Sanitization methods constants
|
|
*/
|
|
final class SanitizationMethod
|
|
{
|
|
public const ABSINT = 'absint';
|
|
public const INTVAL = 'intval';
|
|
public const SANITIZE_EMAIL = 'sanitize_email';
|
|
public const SANITIZE_URL = 'sanitize_url';
|
|
public const ESC_URL_RAW = 'esc_url_raw';
|
|
public const SANITIZE_TEXT_FIELD = 'sanitize_text_field';
|
|
public const SANITIZE_TEXTAREA_FIELD = 'sanitize_textarea_field';
|
|
public const WP_KSES_POST = 'wp_kses_post';
|
|
public const NONE = 'none';
|
|
}
|
|
|
|
/**
|
|
* Escape contexts constants
|
|
*/
|
|
final class EscapeContext
|
|
{
|
|
public const HTML = 'html';
|
|
public const ATTR = 'attr';
|
|
public const URL = 'url';
|
|
public const JS = 'js';
|
|
public const TEXTAREA = 'textarea';
|
|
}
|
|
|
|
/**
|
|
* Security event types constants
|
|
*/
|
|
final class SecurityEventType
|
|
{
|
|
public const AUTHENTICATION_FAILURE = 'auth_failure';
|
|
public const RATE_LIMIT_EXCEEDED = 'rate_limit_exceeded';
|
|
public const INVALID_NONCE = 'invalid_nonce';
|
|
public const UNAUTHORIZED_ACCESS = 'unauthorized_access';
|
|
public const FILE_UPLOAD_ATTEMPT = 'file_upload_attempt';
|
|
public const SUSPICIOUS_REQUEST = 'suspicious_request';
|
|
}
|
|
|
|
/**
|
|
* HVAC_Security_Helpers class
|
|
*
|
|
* Modernized security helpers with PHP 8+ features for enhanced type safety and security
|
|
*/
|
|
final class HVAC_Security_Helpers
|
|
{
|
|
private const DEFAULT_MAX_FILE_SIZE = 5242880; // 5MB
|
|
private const DEFAULT_RATE_LIMIT_WINDOW = 60; // seconds
|
|
private const DEFAULT_MAX_ATTEMPTS = 5;
|
|
|
|
private const IP_HEADER_PRIORITY = [
|
|
'HTTP_CF_CONNECTING_IP',
|
|
'HTTP_CLIENT_IP',
|
|
'HTTP_X_FORWARDED_FOR',
|
|
'REMOTE_ADDR'
|
|
];
|
|
|
|
/**
|
|
* Check if user has HVAC trainer role
|
|
*
|
|
* @param int|null $user_id User ID (null for current user)
|
|
* @return bool True if user has trainer access
|
|
*/
|
|
public static function is_hvac_trainer(?int $user_id = null): bool
|
|
{
|
|
$user = $user_id !== null ? get_user_by('id', $user_id) : wp_get_current_user();
|
|
|
|
if (!$user instanceof \WP_User) {
|
|
return false;
|
|
}
|
|
|
|
$trainer_roles = ['hvac_trainer', 'hvac_master_trainer'];
|
|
$has_trainer_role = !empty(array_intersect($trainer_roles, $user->roles));
|
|
$is_admin = user_can($user, 'manage_options');
|
|
|
|
return $has_trainer_role || $is_admin;
|
|
}
|
|
|
|
/**
|
|
* Check if user has HVAC master trainer role
|
|
*
|
|
* @param int|null $user_id User ID (null for current user)
|
|
* @return bool True if user has master trainer access
|
|
*/
|
|
public static function is_hvac_master_trainer(?int $user_id = null): bool
|
|
{
|
|
$user = $user_id !== null ? get_user_by('id', $user_id) : wp_get_current_user();
|
|
|
|
if (!$user instanceof \WP_User) {
|
|
return false;
|
|
}
|
|
|
|
$has_master_role = in_array('hvac_master_trainer', $user->roles, true);
|
|
$is_admin = user_can($user, 'manage_options');
|
|
|
|
return $has_master_role || $is_admin;
|
|
}
|
|
|
|
/**
|
|
* Sanitize and validate superglobal input
|
|
*
|
|
* @param string $type Superglobal type (use SuperglobalType constants)
|
|
* @param string $key The key to retrieve
|
|
* @param SanitizationMethod|string $sanitize Sanitization method to use
|
|
* @param mixed $default Default value if not set
|
|
* @return mixed Sanitized value
|
|
*/
|
|
public static function get_input(
|
|
string $type,
|
|
string $key,
|
|
string $sanitize = SanitizationMethod::SANITIZE_TEXT_FIELD,
|
|
$default = ''
|
|
) {
|
|
$type_upper = strtoupper($type);
|
|
|
|
// Validate type
|
|
$valid_types = [SuperglobalType::GET, SuperglobalType::POST, SuperglobalType::REQUEST, SuperglobalType::COOKIE, SuperglobalType::SERVER];
|
|
if (!in_array($type_upper, $valid_types, true)) {
|
|
return $default;
|
|
}
|
|
|
|
$superglobal = match ($type_upper) {
|
|
SuperglobalType::GET => $_GET,
|
|
SuperglobalType::POST => $_POST,
|
|
SuperglobalType::REQUEST => $_REQUEST,
|
|
SuperglobalType::COOKIE => $_COOKIE,
|
|
SuperglobalType::SERVER => $_SERVER,
|
|
};
|
|
|
|
if (!isset($superglobal[$key])) {
|
|
return $default;
|
|
}
|
|
|
|
$value = $superglobal[$key];
|
|
|
|
// Handle arrays with proper sanitization
|
|
if (is_array($value)) {
|
|
return self::sanitize_array($value, $sanitize);
|
|
}
|
|
|
|
return self::apply_sanitization($value, $sanitize);
|
|
}
|
|
|
|
/**
|
|
* Sanitize array values recursively
|
|
*
|
|
* @param array $array Array to sanitize
|
|
* @param SanitizationMethod|string $sanitize Sanitization method
|
|
* @return array Sanitized array
|
|
*/
|
|
private static function sanitize_array(array $array, string $sanitize): array
|
|
{
|
|
$sanitized = [];
|
|
|
|
foreach ($array as $key => $value) {
|
|
if (is_array($value)) {
|
|
$sanitized[$key] = self::sanitize_array($value, $sanitize);
|
|
} else {
|
|
$sanitized[$key] = self::apply_sanitization($value, $sanitize);
|
|
}
|
|
}
|
|
|
|
return $sanitized;
|
|
}
|
|
|
|
/**
|
|
* Apply sanitization to a single value
|
|
*
|
|
* @param mixed $value Value to sanitize
|
|
* @param SanitizationMethod|string $sanitize Sanitization method
|
|
* @return mixed Sanitized value
|
|
*/
|
|
private static function apply_sanitization($value, string $sanitize)
|
|
{
|
|
return match ($sanitize) {
|
|
SanitizationMethod::ABSINT => absint($value),
|
|
SanitizationMethod::INTVAL => intval($value),
|
|
SanitizationMethod::SANITIZE_EMAIL => sanitize_email($value),
|
|
SanitizationMethod::SANITIZE_URL, SanitizationMethod::ESC_URL_RAW => esc_url_raw($value),
|
|
SanitizationMethod::SANITIZE_TEXT_FIELD => sanitize_text_field($value),
|
|
SanitizationMethod::SANITIZE_TEXTAREA_FIELD => sanitize_textarea_field($value),
|
|
SanitizationMethod::WP_KSES_POST => wp_kses_post($value),
|
|
SanitizationMethod::NONE => $value, // Use with extreme caution
|
|
default => sanitize_text_field($value), // Safe fallback
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate file upload with enhanced security checks
|
|
*
|
|
* @param array $file $_FILES array element
|
|
* @param array<string> $allowed_types Allowed MIME types
|
|
* @param int $max_size Maximum file size in bytes
|
|
* @return bool|\WP_Error True if valid, WP_Error on failure
|
|
*/
|
|
public static function validate_file_upload(
|
|
array $file,
|
|
array $allowed_types = [],
|
|
int $max_size = self::DEFAULT_MAX_FILE_SIZE
|
|
): bool|\WP_Error {
|
|
// Validate file structure
|
|
$required_keys = ['error', 'tmp_name', 'size', 'name', 'type'];
|
|
foreach ($required_keys as $key) {
|
|
if (!array_key_exists($key, $file)) {
|
|
return new \WP_Error('invalid_file_structure', 'Invalid file upload structure');
|
|
}
|
|
}
|
|
|
|
// Check upload error
|
|
if ($file['error'] !== UPLOAD_ERR_OK) {
|
|
$error_message = match ($file['error']) {
|
|
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'File too large',
|
|
UPLOAD_ERR_PARTIAL => 'File upload incomplete',
|
|
UPLOAD_ERR_NO_FILE => 'No file uploaded',
|
|
UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary directory',
|
|
UPLOAD_ERR_CANT_WRITE => 'Cannot write file to disk',
|
|
UPLOAD_ERR_EXTENSION => 'File upload blocked by extension',
|
|
default => 'File upload failed'
|
|
};
|
|
return new \WP_Error('upload_error', $error_message);
|
|
}
|
|
|
|
// Security: Verify file was actually uploaded via HTTP POST
|
|
if (!is_uploaded_file($file['tmp_name'])) {
|
|
self::log_security_event(SecurityEventType::SUSPICIOUS_REQUEST, [
|
|
'reason' => 'Non-uploaded file in upload validation',
|
|
'file_name' => $file['name'] ?? 'unknown'
|
|
]);
|
|
return new \WP_Error('security_error', 'Invalid file upload');
|
|
}
|
|
|
|
// Validate file size
|
|
if ($file['size'] > $max_size) {
|
|
return new \WP_Error(
|
|
'size_error',
|
|
sprintf('File too large. Maximum size is %s', size_format($max_size))
|
|
);
|
|
}
|
|
|
|
// Empty file check
|
|
if ($file['size'] === 0) {
|
|
return new \WP_Error('empty_file', 'Empty file not allowed');
|
|
}
|
|
|
|
// File type validation
|
|
if (!empty($allowed_types)) {
|
|
$file_type = wp_check_filetype($file['name']);
|
|
|
|
if (empty($file_type['type']) || !in_array($file_type['type'], $allowed_types, true)) {
|
|
return new \WP_Error(
|
|
'type_error',
|
|
sprintf(
|
|
'Invalid file type. Allowed types: %s',
|
|
implode(', ', $allowed_types)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
// Additional security: Check for executable file extensions
|
|
$dangerous_extensions = ['php', 'phtml', 'php3', 'php4', 'php5', 'pl', 'py', 'jsp', 'asp', 'sh', 'cgi'];
|
|
$file_extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
|
|
|
if (in_array($file_extension, $dangerous_extensions, true)) {
|
|
self::log_security_event(SecurityEventType::SUSPICIOUS_REQUEST, [
|
|
'reason' => 'Dangerous file extension upload attempt',
|
|
'file_name' => $file['name'],
|
|
'extension' => $file_extension
|
|
]);
|
|
return new \WP_Error('dangerous_file', 'Executable file types not allowed');
|
|
}
|
|
|
|
self::log_security_event(SecurityEventType::FILE_UPLOAD_ATTEMPT, [
|
|
'file_name' => $file['name'],
|
|
'file_size' => $file['size'],
|
|
'mime_type' => $file['type']
|
|
]);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Generate secure nonce field
|
|
*
|
|
* @param string $action Nonce action
|
|
* @param string $name Nonce field name
|
|
* @return string HTML nonce field
|
|
*/
|
|
public static function nonce_field(string $action, string $name = '_wpnonce'): string
|
|
{
|
|
return wp_nonce_field($action, $name, true, false);
|
|
}
|
|
|
|
/**
|
|
* Verify nonce from request with enhanced security logging
|
|
*
|
|
* @param string $action Nonce action
|
|
* @param string $name Nonce field name
|
|
* @param string $type Request type (use SuperglobalType constants)
|
|
* @return bool True if nonce is valid
|
|
*/
|
|
public static function verify_nonce(
|
|
string $action,
|
|
string $name = '_wpnonce',
|
|
string $type = SuperglobalType::POST
|
|
): bool {
|
|
$nonce = self::get_input($type, $name, SanitizationMethod::SANITIZE_TEXT_FIELD, '');
|
|
|
|
if (empty($nonce)) {
|
|
self::log_security_event(SecurityEventType::INVALID_NONCE, [
|
|
'action' => $action,
|
|
'reason' => 'Empty nonce value'
|
|
]);
|
|
return false;
|
|
}
|
|
|
|
$result = wp_verify_nonce($nonce, $action);
|
|
|
|
if (!$result) {
|
|
self::log_security_event(SecurityEventType::INVALID_NONCE, [
|
|
'action' => $action,
|
|
'nonce' => substr($nonce, 0, 10) . '...' // Log partial nonce for debugging
|
|
]);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check AJAX referer with enhanced error handling and logging
|
|
*
|
|
* @param string $action Nonce action
|
|
* @param string $query_arg Nonce query argument
|
|
* @param bool $send_error Whether to send JSON error response
|
|
* @return bool True if nonce is valid
|
|
*/
|
|
public static function check_ajax_nonce(
|
|
string $action,
|
|
string $query_arg = 'nonce',
|
|
bool $send_error = true
|
|
): bool {
|
|
$result = check_ajax_referer($action, $query_arg, false);
|
|
|
|
if (!$result) {
|
|
self::log_security_event(SecurityEventType::INVALID_NONCE, [
|
|
'action' => $action,
|
|
'context' => 'AJAX request',
|
|
'referer' => $_SERVER['HTTP_REFERER'] ?? 'unknown'
|
|
]);
|
|
|
|
if ($send_error) {
|
|
wp_send_json_error([
|
|
'message' => 'Security check failed',
|
|
'code' => 'invalid_nonce'
|
|
]);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Escape output based on context with type safety
|
|
*
|
|
* @param mixed $data Data to escape
|
|
* @param EscapeContext|string $context Escape context
|
|
* @return string Escaped data
|
|
*/
|
|
public static function escape($data, string $context = EscapeContext::HTML): string
|
|
{
|
|
// Convert complex data types to empty string for security
|
|
if (is_array($data) || is_object($data)) {
|
|
return '';
|
|
}
|
|
|
|
// Convert to string if not already
|
|
$data = (string) $data;
|
|
|
|
return match ($context) {
|
|
EscapeContext::HTML => esc_html($data),
|
|
EscapeContext::ATTR => esc_attr($data),
|
|
EscapeContext::URL => esc_url($data),
|
|
EscapeContext::JS => esc_js($data),
|
|
EscapeContext::TEXTAREA => esc_textarea($data),
|
|
default => esc_html($data), // Safe fallback
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate and sanitize email with enhanced validation
|
|
*
|
|
* @param string $email Email address to validate
|
|
* @return string|false Sanitized email or false if invalid
|
|
*/
|
|
public static function validate_email(string $email)
|
|
{
|
|
// Basic sanitization
|
|
$email = trim($email);
|
|
$email = sanitize_email($email);
|
|
|
|
// WordPress validation
|
|
if (!is_email($email)) {
|
|
return false;
|
|
}
|
|
|
|
// Additional length check (RFC 5321 limit)
|
|
if (strlen($email) > 254) {
|
|
return false;
|
|
}
|
|
|
|
// Check for disposable email domains (basic list)
|
|
$disposable_domains = [
|
|
'10minutemail.com', 'tempmail.org', 'guerrillamail.com',
|
|
'mailinator.com', 'trashmail.com'
|
|
];
|
|
|
|
$domain = substr(strrchr($email, '@'), 1);
|
|
if (in_array(strtolower($domain), $disposable_domains, true)) {
|
|
self::log_security_event(SecurityEventType::SUSPICIOUS_REQUEST, [
|
|
'reason' => 'Disposable email domain detected',
|
|
'domain' => $domain
|
|
]);
|
|
return false;
|
|
}
|
|
|
|
return $email;
|
|
}
|
|
|
|
/**
|
|
* Add comprehensive security headers
|
|
*
|
|
* @param bool $strict Whether to use strict CSP policy
|
|
* @return void
|
|
*/
|
|
public static function add_security_headers(bool $strict = false): void
|
|
{
|
|
// Prevent output if headers already sent
|
|
if (headers_sent()) {
|
|
return;
|
|
}
|
|
|
|
$csp_policy = $strict
|
|
? "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'"
|
|
: "default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval'";
|
|
|
|
$headers = [
|
|
'Content-Security-Policy' => $csp_policy,
|
|
'X-Frame-Options' => 'SAMEORIGIN',
|
|
'X-Content-Type-Options' => 'nosniff',
|
|
'X-XSS-Protection' => '1; mode=block',
|
|
'Referrer-Policy' => 'strict-origin-when-cross-origin',
|
|
'X-Permitted-Cross-Domain-Policies' => 'none',
|
|
'Permissions-Policy' => 'camera=(), microphone=(), geolocation=()'
|
|
];
|
|
|
|
foreach ($headers as $name => $value) {
|
|
header(sprintf('%s: %s', $name, $value));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enhanced rate limiting with exponential backoff
|
|
*
|
|
* @param string $action Action identifier
|
|
* @param int $max_attempts Maximum attempts allowed
|
|
* @param int $window Time window in seconds
|
|
* @return bool True if allowed, false if rate limited
|
|
*/
|
|
public static function check_rate_limit(
|
|
string $action,
|
|
int $max_attempts = self::DEFAULT_MAX_ATTEMPTS,
|
|
int $window = self::DEFAULT_RATE_LIMIT_WINDOW
|
|
): bool {
|
|
$user_id = get_current_user_id();
|
|
$ip = self::get_client_ip();
|
|
$key = sprintf('hvac_rate_limit_%s', hash('sha256', $action . '_' . $user_id . '_' . $ip));
|
|
|
|
$attempts = get_transient($key);
|
|
|
|
if ($attempts === false) {
|
|
set_transient($key, 1, $window);
|
|
return true;
|
|
}
|
|
|
|
if ($attempts >= $max_attempts) {
|
|
// Exponential backoff: extend the window for repeat offenders
|
|
$extended_window = $window * min(8, pow(2, $attempts - $max_attempts));
|
|
set_transient($key, $attempts + 1, $extended_window);
|
|
|
|
self::log_security_event(SecurityEventType::RATE_LIMIT_EXCEEDED, [
|
|
'action' => $action,
|
|
'attempts' => $attempts,
|
|
'window' => $window,
|
|
'extended_window' => $extended_window
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
|
|
set_transient($key, $attempts + 1, $window);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get client IP address with enhanced detection and validation
|
|
*
|
|
* @return string Client IP address
|
|
*/
|
|
public static function get_client_ip(): string
|
|
{
|
|
foreach (self::IP_HEADER_PRIORITY as $header) {
|
|
if (!array_key_exists($header, $_SERVER)) {
|
|
continue;
|
|
}
|
|
|
|
$ip_list = $_SERVER[$header];
|
|
$ips = array_map('trim', explode(',', $ip_list));
|
|
|
|
foreach ($ips as $ip) {
|
|
// Validate IP and exclude private/reserved ranges for public IPs
|
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
|
return $ip;
|
|
}
|
|
|
|
// For local development, accept private ranges
|
|
if (defined('WP_DEBUG') && WP_DEBUG && filter_var($ip, FILTER_VALIDATE_IP)) {
|
|
return $ip;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
|
}
|
|
|
|
/**
|
|
* Enhanced security event logging with structured data
|
|
*
|
|
* @param SecurityEventType|string $event Event type
|
|
* @param array<string, mixed> $data Additional event data
|
|
* @return void
|
|
*/
|
|
public static function log_security_event(string $event, array $data = []): void
|
|
{
|
|
$event_type = $event;
|
|
|
|
$log_data = [
|
|
'event' => $event_type,
|
|
'timestamp' => current_time('mysql', true), // UTC timestamp
|
|
'user_id' => get_current_user_id(),
|
|
'ip' => self::get_client_ip(),
|
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
|
|
'request_uri' => $_SERVER['REQUEST_URI'] ?? '',
|
|
'request_method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
|
|
'data' => $data
|
|
];
|
|
|
|
// Always log to error log with structured format
|
|
error_log(sprintf(
|
|
'[HVAC Security] %s - %s',
|
|
strtoupper($event_type),
|
|
wp_json_encode($log_data, JSON_UNESCAPED_SLASHES)
|
|
));
|
|
|
|
// Optional database logging for persistent storage
|
|
if (defined('HVAC_SECURITY_LOG_TO_DB') && HVAC_SECURITY_LOG_TO_DB) {
|
|
self::log_to_database($event_type, $log_data);
|
|
}
|
|
|
|
// Trigger action hook for extensibility
|
|
do_action('hvac_security_event_logged', $event_type, $log_data);
|
|
}
|
|
|
|
/**
|
|
* Log security event to database with error handling
|
|
*
|
|
* @param string $event_type Event type
|
|
* @param array<string, mixed> $log_data Log data
|
|
* @return void
|
|
*/
|
|
private static function log_to_database(string $event_type, array $log_data): void
|
|
{
|
|
global $wpdb;
|
|
|
|
$table = $wpdb->prefix . 'hvac_security_log';
|
|
|
|
// Check if table exists before attempting insert
|
|
if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)) !== $table) {
|
|
return;
|
|
}
|
|
|
|
$insert_data = [
|
|
'event_type' => $event_type,
|
|
'event_data' => wp_json_encode($log_data['data']),
|
|
'user_id' => $log_data['user_id'],
|
|
'ip_address' => $log_data['ip'],
|
|
'user_agent' => $log_data['user_agent'],
|
|
'request_uri' => $log_data['request_uri'],
|
|
'created_at' => $log_data['timestamp']
|
|
];
|
|
|
|
$result = $wpdb->insert($table, $insert_data);
|
|
|
|
if ($result === false) {
|
|
error_log(sprintf(
|
|
'[HVAC Security] Failed to log to database: %s',
|
|
$wpdb->last_error
|
|
));
|
|
}
|
|
}
|
|
} |