clean build for version 1.4.5 with fixes of security funtionalities, logic branches, etc. Already tested and working fine
This commit is contained in:
@@ -3,215 +3,636 @@
|
||||
class CHECKER_SECURITY {
|
||||
|
||||
/**
|
||||
* Check rate limit for an IP address
|
||||
*
|
||||
* Check rate limit for an IP address using improved method
|
||||
*
|
||||
* @param int $checker_id Checker post ID
|
||||
* @param string $ip IP address to check
|
||||
* @return array ['allowed' => bool, 'message' => string, 'remaining' => int]
|
||||
*/
|
||||
public static function check_rate_limit($checker_id, $ip) {
|
||||
$checker = get_post_meta($checker_id, 'checker', true);
|
||||
|
||||
|
||||
// Check if rate limiting is enabled
|
||||
if (!isset($checker['security']['rate_limit']['enabled']) || $checker['security']['rate_limit']['enabled'] !== 'yes') {
|
||||
return ['allowed' => true, 'remaining' => 999];
|
||||
}
|
||||
|
||||
// Get settings
|
||||
|
||||
// Get settings with defaults
|
||||
$max_attempts = isset($checker['security']['rate_limit']['max_attempts']) ? (int)$checker['security']['rate_limit']['max_attempts'] : 5;
|
||||
$time_window = isset($checker['security']['rate_limit']['time_window']) ? (int)$checker['security']['rate_limit']['time_window'] : 15;
|
||||
$block_duration = isset($checker['security']['rate_limit']['block_duration']) ? (int)$checker['security']['rate_limit']['block_duration'] : 60;
|
||||
$error_message = isset($checker['security']['rate_limit']['error_message']) ? $checker['security']['rate_limit']['error_message'] : 'Too many attempts. Please try again later.';
|
||||
|
||||
// Create transient keys
|
||||
$transient_key = 'checker_rate_' . $checker_id . '_' . md5($ip);
|
||||
$block_key = 'checker_block_' . $checker_id . '_' . md5($ip);
|
||||
|
||||
// Check if IP is blocked
|
||||
|
||||
// Create transient keys with checker-specific prefix
|
||||
$transient_prefix = 'checker_rate_' . $checker_id . '_' . self::get_ip_hash($ip);
|
||||
$transient_key = $transient_prefix . '_attempts';
|
||||
$block_key = $transient_prefix . '_blocked';
|
||||
|
||||
// Check if IP is already blocked
|
||||
$blocked_until = get_transient($block_key);
|
||||
if ($blocked_until !== false) {
|
||||
$remaining_time = ceil(($blocked_until - time()) / 60);
|
||||
// Calculate remaining time
|
||||
$time_remaining = $blocked_until - time();
|
||||
$minutes_remaining = ceil($time_remaining / 60);
|
||||
|
||||
// Build user-friendly message
|
||||
$custom_message = $error_message;
|
||||
if ($custom_message === "Too many attempts. Please try again later.") {
|
||||
$custom_message = sprintf(
|
||||
__('You are temporarily blocked. Please wait %d minutes before trying again.', 'sheet-data-checker-pro'),
|
||||
max(1, $minutes_remaining)
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'allowed' => false,
|
||||
'message' => $error_message . ' (' . $remaining_time . ' minutes remaining)',
|
||||
'remaining' => 0
|
||||
'message' => $custom_message,
|
||||
'remaining' => 0,
|
||||
'blocked_until' => $blocked_until
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// Get current attempts
|
||||
$attempts = get_transient($transient_key);
|
||||
if ($attempts === false) {
|
||||
$attempts = 0;
|
||||
}
|
||||
|
||||
|
||||
// Increment attempts
|
||||
$attempts++;
|
||||
|
||||
|
||||
// Check if exceeded limit
|
||||
if ($attempts > $max_attempts) {
|
||||
// Block the IP
|
||||
set_transient($block_key, time() + ($block_duration * 60), $block_duration * 60);
|
||||
$block_until = time() + ($block_duration * 60);
|
||||
set_transient($block_key, $block_until, $block_duration * 60);
|
||||
// Reset attempts counter
|
||||
delete_transient($transient_key);
|
||||
|
||||
// Log the rate limit block
|
||||
if (class_exists('CHECKER_SECURITY_LOGGER')) {
|
||||
CHECKER_SECURITY_LOGGER::log_rate_limit_block($checker_id, $ip, [
|
||||
'max_attempts' => $max_attempts,
|
||||
'time_window' => $time_window,
|
||||
'block_duration' => $block_duration
|
||||
]);
|
||||
}
|
||||
|
||||
// Build user-friendly error message
|
||||
$minutes_remaining = ceil($block_duration);
|
||||
$custom_message = $error_message;
|
||||
|
||||
// If using default message, enhance it with time info
|
||||
if ($custom_message === "Too many attempts. Please try again later.") {
|
||||
$custom_message = sprintf(
|
||||
__('Too many attempts. Please wait %d minutes before trying again.', 'sheet-data-checker-pro'),
|
||||
$minutes_remaining
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'allowed' => false,
|
||||
'message' => $error_message,
|
||||
'remaining' => 0
|
||||
'message' => $custom_message,
|
||||
'remaining' => 0,
|
||||
'blocked_until' => $block_until
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// Update attempts counter
|
||||
set_transient($transient_key, $attempts, $time_window * 60);
|
||||
|
||||
|
||||
return [
|
||||
'allowed' => true,
|
||||
'remaining' => $max_attempts - $attempts
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Verify reCAPTCHA v3 token
|
||||
*
|
||||
* Verify reCAPTCHA v3 token with improved implementation
|
||||
*
|
||||
* @param int $checker_id Checker post ID
|
||||
* @param string $token reCAPTCHA token from frontend
|
||||
* @param string $action Action name for reCAPTCHA (optional)
|
||||
* @return array ['success' => bool, 'score' => float, 'message' => string]
|
||||
*/
|
||||
public static function verify_recaptcha($checker_id, $token) {
|
||||
public static function verify_recaptcha($checker_id, $token, $action = 'submit') {
|
||||
$checker = get_post_meta($checker_id, 'checker', true);
|
||||
|
||||
|
||||
// Check if reCAPTCHA is enabled
|
||||
if (!isset($checker['security']['recaptcha']['enabled']) || $checker['security']['recaptcha']['enabled'] !== 'yes') {
|
||||
return ['success' => true, 'score' => 1.0];
|
||||
}
|
||||
|
||||
|
||||
// Get settings
|
||||
$secret_key = isset($checker['security']['recaptcha']['secret_key']) ? $checker['security']['recaptcha']['secret_key'] : '';
|
||||
$min_score = isset($checker['security']['recaptcha']['min_score']) ? (float)$checker['security']['recaptcha']['min_score'] : 0.5;
|
||||
|
||||
$secret_key_raw = isset($checker['security']['recaptcha']['secret_key']) ? $checker['security']['recaptcha']['secret_key'] : '';
|
||||
$secret_key = trim((string) $secret_key_raw);
|
||||
|
||||
$min_score_raw = isset($checker['security']['recaptcha']['min_score']) ? $checker['security']['recaptcha']['min_score'] : 0.5;
|
||||
if (is_string($min_score_raw)) {
|
||||
$min_score_raw = str_replace(',', '.', $min_score_raw);
|
||||
}
|
||||
$min_score = (float) $min_score_raw;
|
||||
|
||||
if (empty($secret_key) || empty($token)) {
|
||||
error_log("Sheet Data Checker: reCAPTCHA verification failed - Missing credentials (Checker ID: {$checker_id})");
|
||||
return [
|
||||
'success' => false,
|
||||
'score' => 0,
|
||||
'message' => 'reCAPTCHA verification failed: Missing credentials'
|
||||
];
|
||||
}
|
||||
|
||||
// Verify with Google
|
||||
|
||||
error_log("Sheet Data Checker: Starting reCAPTCHA verification (Checker ID: {$checker_id}, Min Score: {$min_score})");
|
||||
|
||||
// Verify with Google using WordPress HTTP API
|
||||
$response = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', [
|
||||
'timeout' => 10,
|
||||
'body' => [
|
||||
'secret' => $secret_key,
|
||||
'response' => $token
|
||||
'response' => $token,
|
||||
'remoteip' => self::get_client_ip()
|
||||
]
|
||||
]);
|
||||
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
error_log('Sheet Data Checker: reCAPTCHA verification failed - ' . $response->get_error_message());
|
||||
return [
|
||||
'success' => false,
|
||||
'score' => 0,
|
||||
'message' => 'reCAPTCHA verification failed: ' . $response->get_error_message()
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
$body = json_decode(wp_remote_retrieve_body($response), true);
|
||||
|
||||
|
||||
$score = isset($body['score']) ? (float)$body['score'] : 0;
|
||||
$response_action = isset($body['action']) ? $body['action'] : '';
|
||||
|
||||
if (!isset($body['success']) || !$body['success']) {
|
||||
$error_codes = isset($body['error-codes']) ? $body['error-codes'] : 'unknown';
|
||||
error_log("Sheet Data Checker: reCAPTCHA verification failed - Error codes: {$error_codes}");
|
||||
|
||||
// Log the CAPTCHA failure
|
||||
if (class_exists('CHECKER_SECURITY_LOGGER')) {
|
||||
CHECKER_SECURITY_LOGGER::log_captcha_failure($checker_id, 'recaptcha', [
|
||||
'success' => false,
|
||||
'score' => $score,
|
||||
'error_codes' => is_array($body['error-codes']) ? $body['error-codes'] : []
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'score' => 0,
|
||||
'score' => $score,
|
||||
'message' => 'reCAPTCHA verification failed'
|
||||
];
|
||||
}
|
||||
|
||||
$score = isset($body['score']) ? (float)$body['score'] : 0;
|
||||
|
||||
|
||||
// Verify action matches if specified (reCAPTCHA v3 feature)
|
||||
if ($action && $response_action !== $action) {
|
||||
error_log("Sheet Data Checker: reCAPTCHA action mismatch - Expected: {$action}, Got: {$response_action}");
|
||||
return [
|
||||
'success' => false,
|
||||
'score' => $score,
|
||||
'message' => 'reCAPTCHA action verification failed'
|
||||
];
|
||||
}
|
||||
|
||||
if ($score < $min_score) {
|
||||
error_log("Sheet Data Checker: reCAPTCHA score too low - Score: {$score}, Min: {$min_score}");
|
||||
return [
|
||||
'success' => false,
|
||||
'score' => $score,
|
||||
'message' => 'reCAPTCHA score too low. Please try again.'
|
||||
];
|
||||
}
|
||||
|
||||
error_log("Sheet Data Checker: reCAPTCHA verification SUCCESS - Score: {$score}, Action: {$response_action}");
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'score' => $score
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Verify Cloudflare Turnstile token
|
||||
*
|
||||
* Verify Cloudflare Turnstile token with improved implementation
|
||||
*
|
||||
* @param int $checker_id Checker post ID
|
||||
* @param string $token Turnstile token from frontend
|
||||
* @return array ['success' => bool, 'message' => string]
|
||||
*/
|
||||
public static function verify_turnstile($checker_id, $token) {
|
||||
$checker = get_post_meta($checker_id, 'checker', true);
|
||||
|
||||
|
||||
// Check if Turnstile is enabled
|
||||
if (!isset($checker['security']['turnstile']['enabled']) || $checker['security']['turnstile']['enabled'] !== 'yes') {
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
|
||||
// Get settings
|
||||
$secret_key = isset($checker['security']['turnstile']['secret_key']) ? $checker['security']['turnstile']['secret_key'] : '';
|
||||
|
||||
|
||||
if (empty($secret_key) || empty($token)) {
|
||||
error_log("Sheet Data Checker: Turnstile verification failed - Missing credentials (Checker ID: {$checker_id})");
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Turnstile verification failed: Missing credentials'
|
||||
];
|
||||
}
|
||||
|
||||
// Verify with Cloudflare
|
||||
|
||||
error_log("Sheet Data Checker: Starting Turnstile verification (Checker ID: {$checker_id})");
|
||||
|
||||
// Verify with Cloudflare using WordPress HTTP API
|
||||
$response = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
|
||||
'timeout' => 10,
|
||||
'body' => [
|
||||
'secret' => $secret_key,
|
||||
'response' => $token
|
||||
'response' => $token,
|
||||
'remoteip' => self::get_client_ip()
|
||||
]
|
||||
]);
|
||||
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
error_log('Sheet Data Checker: Turnstile verification failed - ' . $response->get_error_message());
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Turnstile verification failed: ' . $response->get_error_message()
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
$body = json_decode(wp_remote_retrieve_body($response), true);
|
||||
|
||||
|
||||
if (!isset($body['success']) || !$body['success']) {
|
||||
$error_codes = isset($body['error-codes']) ? implode(', ', $body['error-codes']) : 'unknown';
|
||||
error_log("Sheet Data Checker: Turnstile verification failed - Error codes: {$error_codes}");
|
||||
|
||||
// Log the CAPTCHA failure
|
||||
if (class_exists('CHECKER_SECURITY_LOGGER')) {
|
||||
CHECKER_SECURITY_LOGGER::log_captcha_failure($checker_id, 'turnstile', [
|
||||
'success' => false,
|
||||
'error_codes' => is_array($body['error-codes']) ? $body['error-codes'] : []
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Turnstile verification failed'
|
||||
];
|
||||
}
|
||||
|
||||
error_log("Sheet Data Checker: Turnstile verification SUCCESS");
|
||||
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get client IP address
|
||||
*
|
||||
* Get client IP address with improved proxy detection
|
||||
*
|
||||
* @return string IP address
|
||||
*/
|
||||
public static function get_client_ip() {
|
||||
$ip = '';
|
||||
// Check for Cloudflare first
|
||||
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
|
||||
return sanitize_text_field($_SERVER['HTTP_CF_CONNECTING_IP']);
|
||||
}
|
||||
|
||||
// Check various proxy headers
|
||||
$ip_headers = [
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_X_REAL_IP',
|
||||
'HTTP_X_FORWARDED',
|
||||
'HTTP_FORWARDED_FOR',
|
||||
'HTTP_FORWARDED',
|
||||
'REMOTE_ADDR'
|
||||
];
|
||||
|
||||
foreach ($ip_headers as $header) {
|
||||
if (!empty($_SERVER[$header])) {
|
||||
$ips = explode(',', $_SERVER[$header]);
|
||||
$ip = trim($ips[0]);
|
||||
|
||||
// Validate IP
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to REMOTE_ADDR
|
||||
return !empty($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : '0.0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hash of the IP for storage (more secure than storing raw IP)
|
||||
*
|
||||
* @param string $ip IP address
|
||||
* @return string Hashed IP
|
||||
*/
|
||||
private static function get_ip_hash($ip) {
|
||||
return wp_hash($ip . 'sheet_checker_rate_limit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify nonce for AJAX requests
|
||||
*
|
||||
* @param string $nonce Nonce value
|
||||
* @param string $action Action name
|
||||
* @param int $checker_id Optional checker ID for logging
|
||||
* @return bool True if valid, false otherwise
|
||||
*/
|
||||
public static function verify_nonce($nonce, $action, $checker_id = 0) {
|
||||
if (!$nonce) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$is_valid = wp_verify_nonce($nonce, $action) !== false;
|
||||
|
||||
// Log nonce failure if checker_id is provided
|
||||
if (!$is_valid && $checker_id && class_exists('CHECKER_SECURITY_LOGGER')) {
|
||||
CHECKER_SECURITY_LOGGER::log_nonce_failure($checker_id, $nonce);
|
||||
}
|
||||
|
||||
return $is_valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if security features are properly configured
|
||||
*
|
||||
* @param int $checker_id Checker post ID
|
||||
* @return array Configuration status
|
||||
*/
|
||||
public static function check_security_config($checker_id) {
|
||||
$checker = get_post_meta($checker_id, 'checker', true);
|
||||
$issues = [];
|
||||
|
||||
// Check rate limiting
|
||||
if (isset($checker['security']['rate_limit']['enabled']) && $checker['security']['rate_limit']['enabled'] === 'yes') {
|
||||
if (!isset($checker['security']['rate_limit']['max_attempts']) || $checker['security']['rate_limit']['max_attempts'] < 1) {
|
||||
$issues[] = 'Rate limiting enabled but max attempts not set or invalid';
|
||||
}
|
||||
|
||||
if (!isset($checker['security']['rate_limit']['time_window']) || $checker['security']['rate_limit']['time_window'] < 1) {
|
||||
$issues[] = 'Rate limiting enabled but time window not set or invalid';
|
||||
}
|
||||
}
|
||||
|
||||
// Check reCAPTCHA
|
||||
if (isset($checker['security']['recaptcha']['enabled']) && $checker['security']['recaptcha']['enabled'] === 'yes') {
|
||||
if (empty($checker['security']['recaptcha']['site_key'])) {
|
||||
$issues[] = 'reCAPTCHA enabled but site key not set';
|
||||
}
|
||||
|
||||
if (empty($checker['security']['recaptcha']['secret_key'])) {
|
||||
$issues[] = 'reCAPTCHA enabled but secret key not set';
|
||||
}
|
||||
}
|
||||
|
||||
// Check Turnstile
|
||||
if (isset($checker['security']['turnstile']['enabled']) && $checker['security']['turnstile']['enabled'] === 'yes') {
|
||||
if (empty($checker['security']['turnstile']['site_key'])) {
|
||||
$issues[] = 'Turnstile enabled but site key not set';
|
||||
}
|
||||
|
||||
if (empty($checker['security']['turnstile']['secret_key'])) {
|
||||
$issues[] = 'Turnstile enabled but secret key not set';
|
||||
}
|
||||
}
|
||||
|
||||
// Check if both CAPTCHAs are enabled
|
||||
$recaptcha_enabled = isset($checker['security']['recaptcha']['enabled']) && $checker['security']['recaptcha']['enabled'] === 'yes';
|
||||
$turnstile_enabled = isset($checker['security']['turnstile']['enabled']) && $checker['security']['turnstile']['enabled'] === 'yes';
|
||||
|
||||
if ($recaptcha_enabled && $turnstile_enabled) {
|
||||
$issues[] = 'Both reCAPTCHA and Turnstile are enabled - only one should be used';
|
||||
}
|
||||
|
||||
return [
|
||||
'configured' => empty($issues),
|
||||
'issues' => $issues
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and validate user input
|
||||
*
|
||||
* @param mixed $value Value to sanitize
|
||||
* @param string $type Type of value (text, email, url, etc.)
|
||||
* @return mixed Sanitized value
|
||||
*/
|
||||
public static function sanitize_input($value, $type = 'text') {
|
||||
if (!is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'email':
|
||||
return sanitize_email($value);
|
||||
case 'url':
|
||||
return esc_url_raw($value);
|
||||
case 'text':
|
||||
default:
|
||||
return sanitize_text_field($value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a security feature is enabled for a checker
|
||||
*
|
||||
* @param array $checker Checker settings array
|
||||
* @param string $feature Feature name: 'recaptcha', 'turnstile', 'rate_limit', 'honeypot'
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_enabled($checker, $feature) {
|
||||
if (!is_array($checker) || !isset($checker['security'])) {
|
||||
return false;
|
||||
}
|
||||
$enabled = $checker['security'][$feature]['enabled'] ?? false;
|
||||
// Accept common truthy flags: 'yes', 'on', true, 1
|
||||
return $enabled === 'yes' || $enabled === 'on' || $enabled === true || $enabled === 1 || $enabled === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get security setting value with default
|
||||
*
|
||||
* @param array $checker Checker settings array
|
||||
* @param string $feature Feature name
|
||||
* @param string $key Setting key
|
||||
* @param mixed $default Default value
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_setting($checker, $feature, $key, $default = '') {
|
||||
if (!is_array($checker) || !isset($checker['security'][$feature][$key])) {
|
||||
return $default;
|
||||
}
|
||||
return $checker['security'][$feature][$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom error message with i18n support
|
||||
*
|
||||
* @param array $checker Checker settings array
|
||||
* @param string $feature Feature name
|
||||
* @param string $default_key Translation key for default message
|
||||
* @return string
|
||||
*/
|
||||
public static function get_error_message($checker, $feature, $default_key = '') {
|
||||
$custom_message = self::get_setting($checker, $feature, 'error_message', '');
|
||||
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
} else {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
if (!empty($custom_message)) {
|
||||
return $custom_message;
|
||||
}
|
||||
|
||||
// Default translatable messages
|
||||
$defaults = [
|
||||
'recaptcha' => __('reCAPTCHA verification failed. Please try again.', 'sheet-data-checker-pro'),
|
||||
'recaptcha_required' => __('reCAPTCHA verification required.', 'sheet-data-checker-pro'),
|
||||
'turnstile' => __('Turnstile verification failed. Please try again.', 'sheet-data-checker-pro'),
|
||||
'turnstile_required' => __('Turnstile verification required.', 'sheet-data-checker-pro'),
|
||||
'rate_limit' => __('Too many attempts. Please try again later.', 'sheet-data-checker-pro'),
|
||||
'honeypot' => __('Security validation failed.', 'sheet-data-checker-pro'),
|
||||
'nonce_expired' => __('Session expired. Please refresh the page and try again.', 'sheet-data-checker-pro'),
|
||||
];
|
||||
|
||||
return isset($defaults[$default_key]) ? $defaults[$default_key] : $defaults[$feature] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified security verification for all CAPTCHA and security checks
|
||||
* Returns error response array if check fails, null if all checks pass
|
||||
*
|
||||
* @param int $checker_id Checker post ID
|
||||
* @param array $checker Checker settings
|
||||
* @param array $request Request data ($_REQUEST)
|
||||
* @param bool $skip_captcha_for_show_all Whether to skip CAPTCHA for show-all mode initial load
|
||||
* @return array|null Error response or null if passed
|
||||
*/
|
||||
public static function verify_all_security($checker_id, $checker, $request, $skip_captcha_for_show_all = false) {
|
||||
$ip = self::get_client_ip();
|
||||
|
||||
// Check honeypot first (fastest check)
|
||||
if (self::is_enabled($checker, 'honeypot')) {
|
||||
$honeypot_value = '';
|
||||
if (isset($request['honeypot_name'], $request['honeypot_value'])) {
|
||||
$honeypot_value = $request['honeypot_value'];
|
||||
} elseif (isset($request['website_url_hp'])) {
|
||||
$honeypot_value = $request['website_url_hp'];
|
||||
}
|
||||
if (!empty($honeypot_value)) {
|
||||
// Log honeypot trigger
|
||||
if (class_exists('CHECKER_SECURITY_LOGGER')) {
|
||||
CHECKER_SECURITY_LOGGER::log_security_event($checker_id, 'honeypot_triggered', [
|
||||
'ip' => $ip
|
||||
]);
|
||||
}
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => self::get_error_message($checker, 'honeypot', 'honeypot'),
|
||||
'type' => 'honeypot'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Check rate limit
|
||||
if (self::is_enabled($checker, 'rate_limit')) {
|
||||
$rate_limit = self::check_rate_limit($checker_id, $ip);
|
||||
if (!$rate_limit['allowed']) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $rate_limit['message'],
|
||||
'type' => 'rate_limit'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Skip CAPTCHA checks for show-all initial load if configured
|
||||
if ($skip_captcha_for_show_all) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If both CAPTCHAs are flagged, prefer Turnstile and skip reCAPTCHA to avoid double validation
|
||||
$turnstile_enabled = self::is_enabled($checker, 'turnstile');
|
||||
$recaptcha_enabled = !$turnstile_enabled && self::is_enabled($checker, 'recaptcha');
|
||||
|
||||
// Check reCAPTCHA if enabled (and Turnstile not enabled)
|
||||
if ($recaptcha_enabled) {
|
||||
$token = isset($request['recaptcha_token']) ? $request['recaptcha_token'] : '';
|
||||
if (empty($token)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => self::get_error_message($checker, 'recaptcha', 'recaptcha_required'),
|
||||
'type' => 'recaptcha'
|
||||
];
|
||||
}
|
||||
$recaptcha_action = isset($checker['security']['recaptcha']['action']) ? $checker['security']['recaptcha']['action'] : 'submit';
|
||||
$recaptcha = self::verify_recaptcha($checker_id, $token, $recaptcha_action);
|
||||
if (!$recaptcha['success']) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => isset($recaptcha['message']) ? $recaptcha['message'] : self::get_error_message($checker, 'recaptcha', 'recaptcha'),
|
||||
'type' => 'recaptcha'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Check Turnstile if enabled
|
||||
if ($turnstile_enabled) {
|
||||
$token = isset($request['turnstile_token']) ? $request['turnstile_token'] : '';
|
||||
if (empty($token)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => self::get_error_message($checker, 'turnstile', 'turnstile_required'),
|
||||
'type' => 'turnstile'
|
||||
];
|
||||
}
|
||||
$turnstile = self::verify_turnstile($checker_id, $token);
|
||||
if (!$turnstile['success']) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => isset($turnstile['message']) ? $turnstile['message'] : self::get_error_message($checker, 'turnstile', 'turnstile'),
|
||||
'type' => 'turnstile'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null; // All checks passed
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if nonce is expired and return appropriate error
|
||||
*
|
||||
* @param string $nonce Nonce value
|
||||
* @param string $action Nonce action
|
||||
* @return array ['valid' => bool, 'expired' => bool, 'message' => string]
|
||||
*/
|
||||
public static function check_nonce_status($nonce, $action = 'checker_ajax_nonce') {
|
||||
$verify = wp_verify_nonce($nonce, $action);
|
||||
|
||||
if ($verify === false) {
|
||||
// Nonce is completely invalid or expired
|
||||
return [
|
||||
'valid' => false,
|
||||
'expired' => true,
|
||||
'message' => __('Session expired. Please refresh the page and try again.', 'sheet-data-checker-pro')
|
||||
];
|
||||
}
|
||||
|
||||
// If multiple IPs, get the first one
|
||||
if (strpos($ip, ',') !== false) {
|
||||
$ip = trim(explode(',', $ip)[0]);
|
||||
if ($verify === 2) {
|
||||
// Nonce is valid but was generated 12-24 hours ago (expiring soon)
|
||||
return [
|
||||
'valid' => true,
|
||||
'expired' => false,
|
||||
'expiring_soon' => true,
|
||||
'message' => ''
|
||||
];
|
||||
}
|
||||
|
||||
return $ip;
|
||||
// Nonce is fully valid (generated within 12 hours)
|
||||
return [
|
||||
'valid' => true,
|
||||
'expired' => false,
|
||||
'expiring_soon' => false,
|
||||
'message' => ''
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,437 +1,439 @@
|
||||
<?php
|
||||
|
||||
class SHEET_DATA_CHECKER_PRO {
|
||||
class SHEET_DATA_CHECKER_PRO
|
||||
{
|
||||
/**
|
||||
* A reference to an instance of this class.
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* A reference to an instance of this class.
|
||||
*/
|
||||
private static $instance;
|
||||
* Returns an instance of this class.
|
||||
*/
|
||||
public static function get_instance()
|
||||
{
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of this class.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
/**
|
||||
* Initializes the plugin by setting filters and administration functions.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
add_action("init", [$this, "create_custom_post_type"]);
|
||||
add_action("admin_enqueue_scripts", [$this, "enqueue_bootstrap_admin"]);
|
||||
|
||||
return self::$instance;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the plugin by setting filters and administration functions.
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
add_action( 'init', [$this, 'create_custom_post_type'] );
|
||||
add_action( 'admin_enqueue_scripts', [$this, 'enqueue_bootstrap_admin'] );
|
||||
|
||||
if (!class_exists('CHECKER_LICENSE')) {
|
||||
include SHEET_CHECKER_PRO_PATH . 'includes/class-License.php';
|
||||
if (!class_exists("CHECKER_LICENSE")) {
|
||||
include SHEET_CHECKER_PRO_PATH . "includes/class-License.php";
|
||||
}
|
||||
|
||||
// Load CAPTCHA helper class
|
||||
if (!class_exists("CHECKER_CAPTCHA_HELPER")) {
|
||||
include SHEET_CHECKER_PRO_PATH .
|
||||
"includes/helpers/class-Captcha-Helper.php";
|
||||
}
|
||||
|
||||
// Load security dashboard
|
||||
if (!class_exists("CHECKER_SECURITY_DASHBOARD")) {
|
||||
include SHEET_CHECKER_PRO_PATH .
|
||||
"admin/class-Security-Dashboard.php";
|
||||
}
|
||||
|
||||
// Load security logger
|
||||
if (!class_exists("CHECKER_SECURITY_LOGGER")) {
|
||||
include SHEET_CHECKER_PRO_PATH .
|
||||
"includes/logs/class-Security-Logger.php";
|
||||
}
|
||||
|
||||
$lis = new CHECKER_LICENSE();
|
||||
|
||||
if(true == $lis->the_lis()){
|
||||
|
||||
add_filter( 'manage_checker_posts_columns', [$this, 'filter_cpt_columns']);
|
||||
add_action( 'manage_checker_posts_custom_column', [$this, 'action_custom_columns_content'], 10, 2 );
|
||||
if (true == $lis->the_lis()) {
|
||||
// Schedule cleanup of old security logs
|
||||
add_action("wp", [$this, "schedule_log_cleanup"]);
|
||||
|
||||
add_action( 'add_meta_boxes', [$this, 'add_checker_metabox'] );
|
||||
add_action( 'save_post_checker', [$this, 'save_checker_metabox'] );
|
||||
add_filter("manage_checker_posts_columns", [
|
||||
$this,
|
||||
"filter_cpt_columns",
|
||||
]);
|
||||
add_action(
|
||||
"manage_checker_posts_custom_column",
|
||||
[$this, "action_custom_columns_content"],
|
||||
10,
|
||||
2,
|
||||
);
|
||||
|
||||
add_action( 'wp_ajax_load_repeater_field_card', [$this, 'load_repeater_field_card'] );
|
||||
add_action( 'wp_ajax_load_output_setting', [$this, 'load_output_setting'] );
|
||||
add_action("add_meta_boxes", [$this, "add_checker_metabox"]);
|
||||
add_action("save_post_checker", [$this, "save_checker_metabox"]);
|
||||
|
||||
if (!class_exists('CHECKER_SHORTCODE')) {
|
||||
require 'class-Shortcode.php';
|
||||
add_action("wp_ajax_load_repeater_field_card", [
|
||||
$this,
|
||||
"load_repeater_field_card",
|
||||
]);
|
||||
add_action("wp_ajax_load_output_setting", [
|
||||
$this,
|
||||
"load_output_setting",
|
||||
]);
|
||||
|
||||
if (!class_exists("CHECKER_SHORTCODE")) {
|
||||
require "class-Shortcode.php";
|
||||
}
|
||||
new CHECKER_SHORTCODE();
|
||||
|
||||
}
|
||||
|
||||
add_action("checker_security_log_cleanup", [
|
||||
$this,
|
||||
"cleanup_security_logs",
|
||||
]);
|
||||
}
|
||||
|
||||
public function create_custom_post_type() {
|
||||
public function create_custom_post_type()
|
||||
{
|
||||
$labels = [
|
||||
"name" => "Checker",
|
||||
"singular_name" => "Checker",
|
||||
"menu_name" => "Checkers",
|
||||
"add_new" => "Add New",
|
||||
"add_new_item" => "Add New Checker",
|
||||
"edit" => "Edit",
|
||||
"edit_item" => "Edit Checker",
|
||||
"new_item" => "New Checker",
|
||||
"view" => "View",
|
||||
"view_item" => "View Checker",
|
||||
"search_items" => "Search Checkers",
|
||||
"not_found" => "No checkers found",
|
||||
"not_found_in_trash" => "No checkers found in trash",
|
||||
"parent" => "Parent Checker",
|
||||
];
|
||||
|
||||
$labels = array(
|
||||
'name' => 'Checker',
|
||||
'singular_name' => 'Checker',
|
||||
'menu_name' => 'Checkers',
|
||||
'add_new' => 'Add New',
|
||||
'add_new_item' => 'Add New Checker',
|
||||
'edit' => 'Edit',
|
||||
'edit_item' => 'Edit Checker',
|
||||
'new_item' => 'New Checker',
|
||||
'view' => 'View',
|
||||
'view_item' => 'View Checker',
|
||||
'search_items' => 'Search Checkers',
|
||||
'not_found' => 'No checkers found',
|
||||
'not_found_in_trash' => 'No checkers found in trash',
|
||||
'parent' => 'Parent Checker'
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'label' => 'Checkers',
|
||||
'description' => 'Checkers for your sheet data',
|
||||
'labels' => $labels,
|
||||
'public' => false,
|
||||
'menu_position' => 4,
|
||||
'menu_icon' => SHEET_CHECKER_PRO_URL .'assets/icons8-validation-menu-icon.png',
|
||||
'supports' => array( 'title' ),
|
||||
'hierarchical' => true,
|
||||
'taxonomies' => array( 'category' ),
|
||||
'has_archive' => false,
|
||||
'rewrite' => array( 'slug' => 'checkers' ),
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => true,
|
||||
'show_in_rest' => false,
|
||||
'query_var' => true,
|
||||
);
|
||||
|
||||
register_post_type( 'checker', $args );
|
||||
$args = [
|
||||
"label" => "Checkers",
|
||||
"description" => "Checkers for your sheet data",
|
||||
"labels" => $labels,
|
||||
"public" => false,
|
||||
"menu_position" => 4,
|
||||
"menu_icon" =>
|
||||
SHEET_CHECKER_PRO_URL .
|
||||
"assets/icons8-validation-menu-icon.png",
|
||||
"supports" => ["title"],
|
||||
"hierarchical" => true,
|
||||
"taxonomies" => ["category"],
|
||||
"has_archive" => false,
|
||||
"rewrite" => ["slug" => "checkers"],
|
||||
"show_ui" => true,
|
||||
"show_in_menu" => true,
|
||||
"show_in_rest" => false,
|
||||
"query_var" => true,
|
||||
];
|
||||
|
||||
register_post_type("checker", $args);
|
||||
}
|
||||
|
||||
public function enqueue_bootstrap_admin() {
|
||||
public function enqueue_bootstrap_admin()
|
||||
{
|
||||
$screen = get_current_screen();
|
||||
|
||||
|
||||
// Check that we are on the 'Checker' post editor screen
|
||||
if ( $screen && $screen->id === 'checker' ) {
|
||||
if ($screen && $screen->id === "checker") {
|
||||
// Enqueue Bootstrap CSS
|
||||
wp_enqueue_style( 'bootstrap', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css' );
|
||||
wp_enqueue_style(
|
||||
"bootstrap",
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css",
|
||||
);
|
||||
// wp_enqueue_style( 'bs-table', 'https://unpkg.com/bootstrap-table@1.22.1/dist/bootstrap-table.min.css' );
|
||||
wp_enqueue_style( 'bs-icon', 'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css' );
|
||||
wp_enqueue_style( 'checker-editor', SHEET_CHECKER_PRO_URL . 'assets/admin-editor.css' );
|
||||
wp_enqueue_style( 'datatables', 'https://cdn.datatables.net/2.2.2/css/dataTables.dataTables.css' );
|
||||
wp_enqueue_style(
|
||||
"bs-icon",
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css",
|
||||
);
|
||||
wp_enqueue_style(
|
||||
"checker-editor",
|
||||
SHEET_CHECKER_PRO_URL .
|
||||
"assets/admin-editor.css?ver=" .
|
||||
SHEET_CHECKER_PRO_VERSION,
|
||||
);
|
||||
wp_enqueue_style(
|
||||
"datatables",
|
||||
"https://cdn.datatables.net/2.2.2/css/dataTables.dataTables.css",
|
||||
);
|
||||
|
||||
// Enqueue Bootstrap JS
|
||||
wp_enqueue_script( 'bootstrap', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js', array( 'jquery' ), '4.5.2', true );
|
||||
wp_enqueue_script( 'handlebarjs', 'https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.8/handlebars.min.js', ['jquery'], '4.7.8', true );
|
||||
wp_enqueue_script(
|
||||
"bootstrap",
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js",
|
||||
["jquery"],
|
||||
"4.5.2",
|
||||
true,
|
||||
);
|
||||
wp_enqueue_script(
|
||||
"handlebarjs",
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.8/handlebars.min.js",
|
||||
["jquery"],
|
||||
"4.7.8",
|
||||
true,
|
||||
);
|
||||
// wp_enqueue_script( 'bs-table', 'https://unpkg.com/bootstrap-table@1.22.1/dist/bootstrap-table.min.js', ['jquery'], '1.22.1', true );
|
||||
wp_enqueue_script( 'checker-editor', SHEET_CHECKER_PRO_URL . 'assets/admin-editor-interactions.js', ['jquery', 'handlebarjs'], true );
|
||||
wp_enqueue_script( 'datatables', 'https://cdn.datatables.net/2.2.2/js/dataTables.js', ['jquery'], true );
|
||||
wp_enqueue_script( 'datatables', 'https://cdn.datatables.net/responsive/3.0.4/js/dataTables.responsive.js', ['jquery'], true );
|
||||
wp_enqueue_script( 'datatables', 'https://cdn.datatables.net/responsive/3.0.4/js/responsive.dataTables.js', ['jquery'], true );
|
||||
wp_enqueue_script(
|
||||
"checker-editor",
|
||||
SHEET_CHECKER_PRO_URL . "assets/admin-editor.js",
|
||||
["jquery", "handlebarjs"],
|
||||
SHEET_CHECKER_PRO_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Pass nonce to admin JavaScript - MUST be after enqueue but before interactions script
|
||||
wp_localize_script("checker-editor", "checkerAdminSecurity", [
|
||||
"nonce" => wp_create_nonce("checker_admin_ajax_nonce"),
|
||||
"ajaxurl" => admin_url("admin-ajax.php"),
|
||||
]);
|
||||
|
||||
wp_enqueue_script(
|
||||
"checker-editor-interactions",
|
||||
SHEET_CHECKER_PRO_URL . "assets/admin-editor-interactions.js",
|
||||
["jquery", "handlebarjs", "checker-editor"],
|
||||
SHEET_CHECKER_PRO_VERSION,
|
||||
true
|
||||
);
|
||||
wp_enqueue_script(
|
||||
"datatables",
|
||||
"https://cdn.datatables.net/2.2.2/js/dataTables.js",
|
||||
["jquery"],
|
||||
true,
|
||||
);
|
||||
wp_enqueue_script(
|
||||
"datatables",
|
||||
"https://cdn.datatables.net/responsive/3.0.4/js/dataTables.responsive.js",
|
||||
["jquery"],
|
||||
true,
|
||||
);
|
||||
wp_enqueue_script(
|
||||
"datatables",
|
||||
"https://cdn.datatables.net/responsive/3.0.4/js/responsive.dataTables.js",
|
||||
["jquery"],
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
wp_enqueue_style( 'checker-editor', SHEET_CHECKER_PRO_URL . 'assets/admin.css' );
|
||||
wp_enqueue_style(
|
||||
"checker-editor",
|
||||
SHEET_CHECKER_PRO_URL .
|
||||
"assets/admin.css?ver=" .
|
||||
SHEET_CHECKER_PRO_VERSION,
|
||||
);
|
||||
}
|
||||
|
||||
public function filter_cpt_columns( $columns ) {
|
||||
public function filter_cpt_columns($columns)
|
||||
{
|
||||
// this will add the column to the end of the array
|
||||
$columns['shortcode'] = 'Shortcode';
|
||||
$columns["shortcode"] = "Shortcode";
|
||||
//add more columns as needed
|
||||
|
||||
|
||||
// as with all filters, we need to return the passed content/variable
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function action_custom_columns_content ( $column_id, $post_id ) {
|
||||
public function action_custom_columns_content($column_id, $post_id)
|
||||
{
|
||||
//run a switch statement for all of the custom columns created
|
||||
switch( $column_id ) {
|
||||
case 'shortcode':
|
||||
echo '<input class="dw-checker-post-table-input" value=\'[checker id="'.$post_id.'"]\' />';
|
||||
break;
|
||||
|
||||
switch ($column_id) {
|
||||
case "shortcode":
|
||||
echo '<input class="dw-checker-post-table-input" value=\'[checker id="' .
|
||||
$post_id .
|
||||
'"]\' />';
|
||||
break;
|
||||
|
||||
//add more items here as needed, just make sure to use the column_id in the filter for each new item.
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public function add_checker_metabox() {
|
||||
|
||||
add_meta_box(
|
||||
'checker_preview',
|
||||
'Preview',
|
||||
[$this, 'preview_checker_metabox'],
|
||||
'checker',
|
||||
'normal',
|
||||
'default'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'checker_options',
|
||||
'Options',
|
||||
[$this, 'render_checker_metabox'],
|
||||
'checker',
|
||||
'normal',
|
||||
'default'
|
||||
);
|
||||
|
||||
if(isset($_GET['post']) && isset($_GET['action'])){
|
||||
add_meta_box(
|
||||
'checker_shortcode',
|
||||
'Shortcode',
|
||||
[$this, 'shortcode_checker_metabox'],
|
||||
'checker',
|
||||
'side',
|
||||
'high'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function shortcode_checker_metabox() {
|
||||
?>
|
||||
<div class="mb-2">Use shortcode below:</div>
|
||||
<input value='[checker id="<?=$_GET['post']?>"]' class="form-control border-dark" readonly>
|
||||
<?php
|
||||
public function add_checker_metabox()
|
||||
{
|
||||
add_meta_box(
|
||||
"dw_checker_preview",
|
||||
"Preview",
|
||||
[$this, "preview_checker_metabox"],
|
||||
"checker",
|
||||
"normal",
|
||||
"high",
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
"dw_checker_setting",
|
||||
"Settings",
|
||||
[$this, "render_checker_metabox"],
|
||||
"checker",
|
||||
"normal",
|
||||
"default",
|
||||
);
|
||||
}
|
||||
|
||||
public function preview_checker_metabox($post) {
|
||||
$checker = get_post_meta( $post->ID, 'checker', true );
|
||||
$checker = wp_parse_args( $checker, [
|
||||
'link' => '',
|
||||
'description' => '',
|
||||
'card' => [
|
||||
'width' => 500,
|
||||
'background' => '#cccccc',
|
||||
'border_radius' => 1,
|
||||
'box_shadow' => '10px 5px 15px -5px',
|
||||
'box_shadow_color' => '#333333',
|
||||
'title' => '#333333',
|
||||
'title_align' => 'left',
|
||||
'description' => '#333333',
|
||||
'description_align' => 'left',
|
||||
'divider' => '#333333',
|
||||
'divider_width' => 1
|
||||
],
|
||||
'field' => [
|
||||
'label' => 'block',
|
||||
'label-color' => '#333333'
|
||||
],
|
||||
'fields' => [],
|
||||
'search_button' => [
|
||||
'text' => 'Search',
|
||||
'bg_color' => '#cccccc',
|
||||
'text_color' => '#333333',
|
||||
'position' => 'flex-end'
|
||||
],
|
||||
'back_button' => [
|
||||
'text' => 'Back',
|
||||
'bg_color' => '#cccccc',
|
||||
'text_color' => '#333333',
|
||||
'position' => 'flex-start'
|
||||
],
|
||||
'result' => [
|
||||
'display' => 'tabel',
|
||||
'header' => '#333333',
|
||||
'value' => '#333333',
|
||||
'columns' => [],
|
||||
'border_width' => 1
|
||||
]
|
||||
] );
|
||||
|
||||
require_once SHEET_CHECKER_PRO_PATH . 'templates/editor/preview.php';
|
||||
}
|
||||
|
||||
public function render_checker_metabox( $post ) {
|
||||
// Retrieve existing values from the database
|
||||
$checker = get_post_meta( $post->ID, 'checker', true );
|
||||
$checker = wp_parse_args( $checker, [
|
||||
'link' => '',
|
||||
'description' => '',
|
||||
'card' => [
|
||||
'width' => 500,
|
||||
'background' => '#cccccc',
|
||||
'bg_opacity' => 100,
|
||||
'border_radius' => 1,
|
||||
'box_shadow' => '10px 5px 15px -5px',
|
||||
'box_shadow_color' => '#333333',
|
||||
'title' => '#333333',
|
||||
'title_align' => 'left',
|
||||
'description' => '#333333',
|
||||
'description_align' => 'left',
|
||||
'divider' => '#333333',
|
||||
'divider_width' => 1
|
||||
],
|
||||
'field' => [
|
||||
'label' => 'block',
|
||||
'label-color' => '#333333'
|
||||
],
|
||||
'fields' => [],
|
||||
'search_button' => [
|
||||
'text' => 'Search',
|
||||
'bg_color' => '#cccccc',
|
||||
'text_color' => '#333333',
|
||||
'position' => 'flex-end'
|
||||
],
|
||||
'back_button' => [
|
||||
'text' => 'Back',
|
||||
'bg_color' => '#cccccc',
|
||||
'text_color' => '#333333',
|
||||
'position' => 'flex-start'
|
||||
],
|
||||
'result' => [
|
||||
'display' => 'table',
|
||||
'header' => '#333333',
|
||||
'value' => '#333333',
|
||||
'divider' => '#333333',
|
||||
'divider_width' => 1
|
||||
]
|
||||
] );
|
||||
|
||||
require_once SHEET_CHECKER_PRO_PATH . 'templates/editor/settings.php';
|
||||
}
|
||||
|
||||
public function save_checker_metabox( $post_id ) {
|
||||
public function save_checker_metabox($post_id)
|
||||
{
|
||||
// Save metabox data
|
||||
if ( isset( $_POST['checker'] ) ) {
|
||||
error_log(print_r($_POST['checker'], true));
|
||||
update_post_meta( $post_id, 'checker', $_POST['checker'] );
|
||||
if (isset($_POST["checker"])) {
|
||||
$checker = $_POST["checker"];
|
||||
// Sanitize all values to prevent null deprecation warnings
|
||||
$checker = $this->sanitize_array_recursive($checker);
|
||||
update_post_meta($post_id, "checker", $checker);
|
||||
}
|
||||
}
|
||||
|
||||
public function load_repeater_field_card_depracated() {
|
||||
|
||||
$post_id = $_REQUEST['pid'];
|
||||
$checker = get_post_meta( $post_id, 'checker', true );
|
||||
$json = json_decode(stripslashes($_REQUEST['json']), true);
|
||||
|
||||
if(isset($checker['fields']) && count($checker['fields']) > 0){
|
||||
foreach($checker['fields'] as $key => $field){
|
||||
?>
|
||||
<div class="card shadow repeater-card gap-2 position-relative">
|
||||
<div class="card-body">
|
||||
<div class="row mb-2">
|
||||
<div class="col-3"><label class="form-label fw-bold mb-0">Field ID</label></div>
|
||||
<div class="col-9">
|
||||
<input class="form-control field-id" value="<?= $key ?>" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-3"><label class="form-label fw-bold mb-0">Column</label></div>
|
||||
<div class="col-9">
|
||||
<select name="checker[fields][<?= $key ?>][kolom]" class="form-select border select-kolom">
|
||||
<?php
|
||||
if($json){
|
||||
$header = $this->parse_header_kolom($json);
|
||||
if(!empty($header)){
|
||||
foreach($header as $name){
|
||||
if( $field['kolom'] == $name ){
|
||||
echo '<option value="'.$name.'" selected>'.$name.'</option>';
|
||||
}else{
|
||||
echo '<option value="'.$name.'">'.$name.'</option>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-3"><label class="form-label fw-bold mb-0">Type</label></div>
|
||||
<div class="col-9">
|
||||
<select name="checker[fields][<?= $key ?>][type]" class="form-select border select-field-type">
|
||||
<option value="text" <?= ($field['type'] == 'text') ? 'selected' : '' ?>>Text</option>
|
||||
<option value="select" <?= ($field['type'] == 'select') ? 'selected' : '' ?>>Select</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-3"><label class="form-label fw-bold mb-0">Label</label></div>
|
||||
<div class="col-9">
|
||||
<input name="checker[fields][<?= $key ?>][label]" class="form-control field-label" value="<?= $field['label'] ?? '' ?>" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-3"><label class="form-label fw-bold mb-0">Placeholder</label></div>
|
||||
<div class="col-9">
|
||||
<input name="checker[fields][<?= $key ?>][placeholder]" class="form-control field-placeholder" value="<?= $field['placeholder'] ?? '' ?>" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-3"><label class="form-label fw-bold mb-0">Value Matcher</label></div>
|
||||
<div class="col-9">
|
||||
<select name="checker[fields][<?= $key ?>][match]" class="form-select border select-match-type">
|
||||
<option value="match" <?= ($field['match'] == 'match') ? 'selected' : '' ?>>Match</option>
|
||||
<option value="contain" <?= ($field['match'] == 'contain') ? 'selected' : '' ?>>Contain</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-buttons d-flex gap-2 flex-column position-absolute">
|
||||
<button type="button" class="btn btn-primary py-1 px-2 add-form-card"><i class="bi bi-plus"></i></button>
|
||||
<button type="button" class="btn btn-danger py-1 px-2 delete-form-card"><i class="bi bi-dash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
/**
|
||||
* Recursively sanitize array values to prevent null deprecation warnings
|
||||
* Converts null values to empty strings
|
||||
*
|
||||
* @param mixed $data Data to sanitize
|
||||
* @return mixed Sanitized data
|
||||
*/
|
||||
private function sanitize_array_recursive($data)
|
||||
{
|
||||
if (is_array($data)) {
|
||||
foreach ($data as $key => $value) {
|
||||
$data[$key] = $this->sanitize_array_recursive($value);
|
||||
}
|
||||
}else{
|
||||
?>
|
||||
<div class="card shadow repeater-card gap-2 position-relative">
|
||||
<div class="card-body">
|
||||
<div class="row mb-2">
|
||||
<div class="col-3"><label class="form-label fw-bold mb-0">Field ID</label></div>
|
||||
<div class="col-9">
|
||||
<input class="form-control field-id" value="" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-3"><label class="form-label fw-bold mb-0">Column</label></div>
|
||||
<div class="col-9">
|
||||
<select name="" class="form-select border select-kolom">
|
||||
<?php
|
||||
if($json){
|
||||
$header = $this->parse_header_kolom($json);
|
||||
if(!empty($header)){
|
||||
foreach($header as $key => $name){
|
||||
if( $key == 0 ){
|
||||
echo '<option value="'.$name.'" selected>'.$name.'</option>';
|
||||
}else{
|
||||
echo '<option value="'.$name.'">'.$name.'</option>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-3"><label class="form-label fw-bold mb-0">Type</label></div>
|
||||
<div class="col-9">
|
||||
<select name="" class="form-select border select-field-type">
|
||||
<option value="text" selected>Text</option>
|
||||
<option value="select">Select</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-3"><label class="form-label fw-bold mb-0">Label</label></div>
|
||||
<div class="col-9">
|
||||
<input name="" class="form-control field-label" value="" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-3"><label class="form-label fw-bold mb-0">Placeholder</label></div>
|
||||
<div class="col-9">
|
||||
<input name="" class="form-control field-placeholder" value="" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-3"><label class="form-label fw-bold mb-0">Value Matcher</label></div>
|
||||
<div class="col-9">
|
||||
<select name="" class="form-select border select-match-type">
|
||||
<option value="match" selected>Match</option>
|
||||
<option value="contain">Contain</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-buttons d-flex gap-2 flex-column position-absolute">
|
||||
<button type="button" class="btn btn-primary py-1 px-2 add-form-card"><i class="bi bi-plus"></i></button>
|
||||
<button type="button" class="btn btn-danger py-1 px-2 delete-form-card"><i class="bi bi-dash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return $data;
|
||||
}
|
||||
exit();
|
||||
|
||||
// Convert null to empty string
|
||||
if ($data === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function load_repeater_field_card() {
|
||||
$post_id = $_REQUEST['pid'];
|
||||
/**
|
||||
* Recursively merge two arrays, with the second array's values taking precedence
|
||||
* Unlike array_merge_recursive, this doesn't create arrays for scalar values
|
||||
*
|
||||
* @param array $defaults Default values
|
||||
* @param array $args Values to merge
|
||||
* @return array Merged array
|
||||
*/
|
||||
private function array_merge_recursive_distinct(array $defaults, array $args)
|
||||
{
|
||||
$merged = $defaults;
|
||||
foreach ($args as $key => $value) {
|
||||
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
|
||||
$merged[$key] = $this->array_merge_recursive_distinct($merged[$key], $value);
|
||||
} else {
|
||||
$merged[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $merged;
|
||||
}
|
||||
|
||||
public function render_checker_metabox($post)
|
||||
{
|
||||
// Retrieve existing values from the database
|
||||
$checker = get_post_meta($post->ID, "checker", true);
|
||||
$post_id = $post->ID;
|
||||
|
||||
// Define default values - include ALL keys that templates access
|
||||
$defaults = [
|
||||
"link" => "",
|
||||
"description" => "",
|
||||
"card" => [
|
||||
"width" => 500,
|
||||
"background" => "#ffffff",
|
||||
"title" => "#333333",
|
||||
"description" => "#666666",
|
||||
"divider" => "#cccccc",
|
||||
"divider_width" => 1,
|
||||
"title_align" => "center",
|
||||
"description_align" => "center",
|
||||
],
|
||||
"field" => [
|
||||
"label" => "block",
|
||||
"label-color" => "#333333",
|
||||
],
|
||||
"fields" => [],
|
||||
"search_button" => [
|
||||
"position" => "flex-start",
|
||||
"text" => "Search",
|
||||
"bg_color" => "#333333",
|
||||
"text_color" => "#ffffff",
|
||||
],
|
||||
"back_button" => [
|
||||
"position" => "flex-start",
|
||||
"text" => "Back",
|
||||
"bg_color" => "#333333",
|
||||
"text_color" => "#ffffff",
|
||||
],
|
||||
"result" => [
|
||||
"initial_display" => "hidden",
|
||||
"filter_mode" => "search",
|
||||
"max_records" => 100,
|
||||
"display" => "vertical-table",
|
||||
"header" => "#333333",
|
||||
"value" => "#666666",
|
||||
"divider" => "#cccccc",
|
||||
"divider_width" => 1,
|
||||
"card_grid" => [
|
||||
"desktop" => 4,
|
||||
"tablet" => 2,
|
||||
"mobile" => 1,
|
||||
],
|
||||
],
|
||||
"url_params" => [
|
||||
"enabled" => "no",
|
||||
],
|
||||
"output" => [],
|
||||
];
|
||||
|
||||
// Parse and merge with defaults (deep merge for nested arrays)
|
||||
$checker = is_array($checker) ? $this->array_merge_recursive_distinct($defaults, $checker) : $defaults;
|
||||
|
||||
require_once SHEET_CHECKER_PRO_PATH . "templates/editor/settings.php";
|
||||
}
|
||||
|
||||
public function preview_checker_metabox($post)
|
||||
{
|
||||
// Retrieve existing values from the database
|
||||
$checker = get_post_meta($post->ID, "checker", true);
|
||||
|
||||
// Define default values
|
||||
$defaults = [
|
||||
"link" => "",
|
||||
"description" => "",
|
||||
"card" => [
|
||||
"width" => 500,
|
||||
"background" => "#ffffff",
|
||||
"title" => "#333333",
|
||||
"description" => "#666666",
|
||||
"divider" => "#cccccc",
|
||||
"divider_width" => 1,
|
||||
],
|
||||
];
|
||||
|
||||
// Parse and merge with defaults
|
||||
$checker = wp_parse_args($checker, $defaults);
|
||||
|
||||
require_once SHEET_CHECKER_PRO_PATH . "templates/editor/preview.php";
|
||||
}
|
||||
|
||||
public function load_repeater_field_card()
|
||||
{
|
||||
$nonce_ok = check_ajax_referer('checker_admin_ajax_nonce', 'security', false);
|
||||
if (false === $nonce_ok && !current_user_can('edit_posts')) {
|
||||
wp_send_json_error('invalid_nonce', 403);
|
||||
}
|
||||
|
||||
$post_id = isset($_REQUEST['pid']) ? absint($_REQUEST['pid']) : 0;
|
||||
|
||||
// Require capability for existing posts; for new posts rely on logged-in nonce
|
||||
if ($post_id && !current_user_can('edit_posts')) {
|
||||
wp_send_json_error('Unauthorized request', 403);
|
||||
}
|
||||
if (!$post_id && !is_user_logged_in()) {
|
||||
wp_send_json_error('Unauthorized request', 403);
|
||||
}
|
||||
|
||||
$checker = get_post_meta($post_id, 'checker', true);
|
||||
$headers = $_REQUEST['headers'];
|
||||
$headers_raw = isset($_REQUEST['headers']) ? (array) $_REQUEST['headers'] : [];
|
||||
$headers = array_map('sanitize_text_field', $headers_raw);
|
||||
|
||||
error_log('[REPEATER] Post ID: ' . $post_id);
|
||||
error_log('[REPEATER] Has fields: ' . (isset($checker['fields']) && count($checker['fields']) > 0 ? 'YES' : 'NO'));
|
||||
error_log('[REPEATER] Headers count: ' . (is_array($headers) ? count($headers) : '0'));
|
||||
|
||||
$response = [];
|
||||
|
||||
@@ -440,52 +442,53 @@ class SHEET_DATA_CHECKER_PRO {
|
||||
$response[$key] = $field;
|
||||
|
||||
$rowHeader = [];
|
||||
foreach($headers as $index => $header){
|
||||
$id = '_'.strtolower($header);
|
||||
$rowHeader[$index] = $id;
|
||||
if (is_array($headers)) {
|
||||
foreach($headers as $index => $header){
|
||||
$id = '_'.strtolower($header);
|
||||
$rowHeader[$index] = $id;
|
||||
}
|
||||
}
|
||||
$response[$key]['selected_kolom'] = $response[$key]['kolom'];
|
||||
$response[$key]['selected_kolom'] = isset($response[$key]['kolom']) ? $response[$key]['kolom'] : '';
|
||||
$response[$key]['kolom'] = $headers;
|
||||
}
|
||||
} else {
|
||||
// No saved fields - create one default field
|
||||
error_log('[REPEATER] Creating default field');
|
||||
$response['field_1'] = [
|
||||
'type' => 'text',
|
||||
'label' => '',
|
||||
'placeholder' => '',
|
||||
'match' => 'match',
|
||||
'kolom' => $headers,
|
||||
'selected_kolom' => is_array($headers) && count($headers) > 0 ? $headers[0] : ''
|
||||
];
|
||||
}
|
||||
|
||||
wp_send_json($response);
|
||||
exit();
|
||||
error_log('[REPEATER] Response keys: ' . print_r(array_keys($response), true));
|
||||
wp_send_json_success(['fields' => $response]);
|
||||
}
|
||||
|
||||
public function load_column_checkbox() {
|
||||
|
||||
$post_id = $_REQUEST['pid'];
|
||||
$checker = get_post_meta( $post_id, 'checker', true );
|
||||
$json = json_decode(stripslashes($_REQUEST['json']), true);
|
||||
|
||||
$header = $this->parse_header_kolom($json);
|
||||
|
||||
if(count($header) > 0){
|
||||
foreach($header as $key){
|
||||
$checked = '';
|
||||
if(isset($checker['result']['columns']) && in_array($key, $checker['result']['columns'])){
|
||||
$checked = ' checked';
|
||||
}
|
||||
?>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="<?= $key ?>" id="checker-item-<?= strtolower(str_replace(' ', '_', $key)) ?>" name="checker[result][columns][]"<?=$checked?>>
|
||||
<label class="form-check-label" for="checker-item-<?= strtolower(str_replace(' ', '_', $key)) ?>">
|
||||
<?= $key ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
public function load_output_setting()
|
||||
{
|
||||
$nonce_ok = check_ajax_referer('checker_admin_ajax_nonce', 'security', false);
|
||||
if (false === $nonce_ok && !current_user_can('edit_posts')) {
|
||||
wp_send_json_error('invalid_nonce', 403);
|
||||
}
|
||||
exit();
|
||||
|
||||
}
|
||||
$post_id = isset($_REQUEST['pid']) ? absint($_REQUEST['pid']) : 0;
|
||||
|
||||
// Require capability for existing posts; for new posts rely on logged-in nonce
|
||||
if ($post_id && !current_user_can('edit_posts')) {
|
||||
wp_send_json_error('Unauthorized request', 403);
|
||||
}
|
||||
if (!$post_id && !is_user_logged_in()) {
|
||||
wp_send_json_error('Unauthorized request', 403);
|
||||
}
|
||||
|
||||
$checker = $post_id ? get_post_meta($post_id, 'checker', true) : [];
|
||||
$headers_raw = isset($_REQUEST['headers']) ? (array) $_REQUEST['headers'] : [];
|
||||
$headers = array_map('sanitize_text_field', $headers_raw);
|
||||
|
||||
public function load_output_setting() {
|
||||
$post_id = $_REQUEST['pid'];
|
||||
$checker = get_post_meta($post_id, 'checker', true);
|
||||
$headers = $_REQUEST['headers'];
|
||||
|
||||
// $header = $this->parse_header_kolom($json);
|
||||
|
||||
if (!empty($headers)) {
|
||||
@@ -509,41 +512,54 @@ class SHEET_DATA_CHECKER_PRO {
|
||||
|
||||
wp_send_json_success(['data' => $output_data]);
|
||||
} else {
|
||||
wp_send_json_error('No headers found');
|
||||
wp_send_json_error("No headers found");
|
||||
}
|
||||
exit();
|
||||
}
|
||||
|
||||
|
||||
public function parse_options($json, $kolom) {
|
||||
public function parse_header_kolom($json)
|
||||
{
|
||||
if (!is_array($json)) {
|
||||
$json = json_decode($json, true);
|
||||
}
|
||||
$header = array_keys($json[0]);
|
||||
return $header;
|
||||
}
|
||||
|
||||
$json = json_decode($json, true);
|
||||
public function parse_options($json, $kolom)
|
||||
{
|
||||
$options = [];
|
||||
if($json){
|
||||
foreach($json as $key => $value){
|
||||
foreach($value as $name => $val){
|
||||
if($name == $kolom){
|
||||
if(!in_array($val, $options)){
|
||||
if ($json) {
|
||||
foreach ($json as $key => $value) {
|
||||
foreach ($value as $name => $val) {
|
||||
if ($name == $kolom) {
|
||||
if (!in_array($val, $options)) {
|
||||
$options[] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
|
||||
}
|
||||
|
||||
public function parse_header_kolom($json) {
|
||||
|
||||
$header = [];
|
||||
if(!is_array($json)){
|
||||
$json = json_decode($json, true);
|
||||
/**
|
||||
* Schedule cleanup of old security logs
|
||||
*/
|
||||
public function schedule_log_cleanup()
|
||||
{
|
||||
// Schedule cleanup if not already scheduled
|
||||
if (!wp_next_scheduled("checker_security_log_cleanup")) {
|
||||
wp_schedule_event(time(), "daily", "checker_security_log_cleanup");
|
||||
}
|
||||
$header = array_keys($json[0]);
|
||||
return $header;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Cleanup old security logs
|
||||
*/
|
||||
public static function cleanup_security_logs()
|
||||
{
|
||||
if (class_exists("CHECKER_SECURITY_LOGGER")) {
|
||||
CHECKER_SECURITY_LOGGER::cleanup_old_logs(90); // Keep logs for 90 days
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
431
includes/helpers/class-Captcha-Helper.php
Normal file
431
includes/helpers/class-Captcha-Helper.php
Normal file
@@ -0,0 +1,431 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* CAPTCHA Helper Class
|
||||
*
|
||||
* Handles integration with reCAPTCHA v3 and Cloudflare Turnstile
|
||||
* Provides methods for script loading, token generation, and verification
|
||||
*
|
||||
* @since 1.5.0
|
||||
*/
|
||||
|
||||
class CHECKER_CAPTCHA_HELPER {
|
||||
|
||||
/**
|
||||
* Load CAPTCHA scripts and initialize on page
|
||||
*
|
||||
* @param int $checker_id Checker post ID
|
||||
* @return void
|
||||
*/
|
||||
public static function load_captcha_scripts($checker_id) {
|
||||
$checker = get_post_meta($checker_id, 'checker', true);
|
||||
|
||||
if (!$checker) {
|
||||
error_log("CAPTCHA Helper: No checker meta found for ID: {$checker_id}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Turnstile is enabled
|
||||
$turnstile_flag = $checker['security']['turnstile']['enabled'] ?? false;
|
||||
$turnstile_enabled = ($turnstile_flag === 'yes' || $turnstile_flag === 'on' || $turnstile_flag === true || $turnstile_flag === 1 || $turnstile_flag === '1');
|
||||
|
||||
// If Turnstile is enabled, prefer it and skip loading reCAPTCHA to avoid dual-widgets
|
||||
$recaptcha_flag = $checker['security']['recaptcha']['enabled'] ?? false;
|
||||
$recaptcha_enabled = !$turnstile_enabled && ($recaptcha_flag === 'yes' || $recaptcha_flag === 'on' || $recaptcha_flag === true || $recaptcha_flag === 1 || $recaptcha_flag === '1');
|
||||
|
||||
if ($turnstile_enabled) {
|
||||
// Make sure any previously enqueued reCAPTCHA is disabled to avoid dual widgets
|
||||
if (wp_script_is('google-recaptcha-v3', 'enqueued') || wp_script_is('google-recaptcha-v3', 'to_enqueue')) {
|
||||
wp_dequeue_script('google-recaptcha-v3');
|
||||
wp_deregister_script('google-recaptcha-v3');
|
||||
}
|
||||
// Also dequeue any generic recaptcha handle some themes/plugins may use
|
||||
if (wp_script_is('recaptcha', 'enqueued') || wp_script_is('recaptcha', 'to_enqueue')) {
|
||||
wp_dequeue_script('recaptcha');
|
||||
wp_deregister_script('recaptcha');
|
||||
}
|
||||
// Ensure the main frontend bundle ignores reCAPTCHA when Turnstile is enabled and hide any badge
|
||||
wp_add_inline_script(
|
||||
'checker-pro',
|
||||
'window.checkerRecaptcha = null; (function(){var b=document.querySelector(".grecaptcha-badge"); if(b){b.style.display=\"none\";}})();',
|
||||
'before'
|
||||
);
|
||||
|
||||
$site_key = $checker['security']['turnstile']['site_key'] ?? '';
|
||||
$theme = $checker['security']['turnstile']['theme'] ?? 'auto';
|
||||
$size = $checker['security']['turnstile']['size'] ?? 'normal';
|
||||
|
||||
if ($site_key) {
|
||||
self::load_turnstile_script($site_key, $theme, $size);
|
||||
}
|
||||
} elseif ($recaptcha_enabled) {
|
||||
$site_key = $checker['security']['recaptcha']['site_key'] ?? '';
|
||||
$action = $checker['security']['recaptcha']['action'] ?? 'checker_validate';
|
||||
$hide_badge = $checker['security']['recaptcha']['hide_badge'] ?? 'no';
|
||||
|
||||
if ($site_key) {
|
||||
self::load_recaptcha_v3_script($site_key, $action, $hide_badge === 'yes');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load reCAPTCHA v3 script and initialize
|
||||
*
|
||||
* @param string $site_key reCAPTCHA site key
|
||||
* @param string $action Action name for verification
|
||||
* @param bool $hide_badge Whether to hide the reCAPTCHA badge
|
||||
* @return void
|
||||
*/
|
||||
private static function load_recaptcha_v3_script($site_key, $action, $hide_badge = false) {
|
||||
// Enqueue reCAPTCHA v3 script
|
||||
wp_enqueue_script(
|
||||
'google-recaptcha-v3',
|
||||
'https://www.google.com/recaptcha/api.js?render=' . $site_key,
|
||||
[],
|
||||
'3.0',
|
||||
true
|
||||
);
|
||||
|
||||
// Pass settings to JavaScript
|
||||
wp_add_inline_script(
|
||||
'google-recaptcha-v3',
|
||||
'
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
console.log("[CAPTCHA Debug] DOMContentLoaded - checking for grecaptcha...");
|
||||
console.log("[CAPTCHA Debug] grecaptcha available:", typeof grecaptcha !== "undefined");
|
||||
|
||||
if (typeof grecaptcha !== "undefined") {
|
||||
console.log("[CAPTCHA Debug] reCAPTCHA v3 detected, initializing...");
|
||||
|
||||
// Store reCAPTCHA settings globally
|
||||
window.checkerRecaptcha = {
|
||||
siteKey: "' . esc_js($site_key) . '",
|
||||
action: "' . esc_js($action) . '"
|
||||
};
|
||||
|
||||
console.log("[CAPTCHA Debug] reCAPTCHA settings:", window.checkerRecaptcha);
|
||||
|
||||
' . ($hide_badge ? '
|
||||
// Hide reCAPTCHA badge
|
||||
var style = document.createElement("style");
|
||||
style.innerHTML = ".grecaptcha-badge { display: none !important; }";
|
||||
document.head.appendChild(style);
|
||||
console.log("[CAPTCHA Debug] reCAPTCHA badge hidden");
|
||||
' : '
|
||||
// Ensure badge is visible
|
||||
var style = document.createElement("style");
|
||||
style.innerHTML = ".grecaptcha-badge { visibility: visible !important; opacity: 1 !important; display: block !important; }";
|
||||
document.head.appendChild(style);
|
||||
') . '
|
||||
|
||||
// Initialize reCAPTCHA for all checker forms
|
||||
initRecaptchaForForms();
|
||||
console.log("[CAPTCHA Debug] reCAPTCHA initialization complete");
|
||||
} else {
|
||||
console.error("[CAPTCHA Debug] grecaptcha NOT available - script may not have loaded");
|
||||
}
|
||||
});
|
||||
|
||||
function initRecaptchaForForms() {
|
||||
var forms = document.querySelectorAll(".dw-checker-container form");
|
||||
forms.forEach(function(form) {
|
||||
var searchButton = form.querySelector(".search-button");
|
||||
if (!searchButton) return;
|
||||
|
||||
// Generate token when search button is clicked
|
||||
searchButton.addEventListener("click", function(e) {
|
||||
var tokenInput = form.querySelector("input[name=recaptcha_token]");
|
||||
|
||||
// If token already exists and is recent (less than 2 minutes old), don\'t regenerate
|
||||
if (tokenInput && tokenInput.value && tokenInput.dataset.timestamp) {
|
||||
var tokenAge = Date.now() - parseInt(tokenInput.dataset.timestamp);
|
||||
if (tokenAge < 120000) { // 2 minutes
|
||||
console.log("reCAPTCHA: Using existing token (age: " + Math.floor(tokenAge/1000) + "s)");
|
||||
return; // Let the click proceed with existing token
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent default to wait for token generation
|
||||
var hasToken = tokenInput && tokenInput.value;
|
||||
if (!hasToken) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
console.log("reCAPTCHA: Generating new token...");
|
||||
|
||||
// Generate new token
|
||||
grecaptcha.ready(function() {
|
||||
grecaptcha.execute(
|
||||
window.checkerRecaptcha.siteKey,
|
||||
{action: window.checkerRecaptcha.action}
|
||||
).then(function(token) {
|
||||
console.log("reCAPTCHA: Token generated successfully");
|
||||
// Add or update token in hidden input
|
||||
if (!tokenInput) {
|
||||
tokenInput = document.createElement("input");
|
||||
tokenInput.type = "hidden";
|
||||
tokenInput.name = "recaptcha_token";
|
||||
form.appendChild(tokenInput);
|
||||
}
|
||||
tokenInput.value = token;
|
||||
tokenInput.dataset.timestamp = Date.now().toString();
|
||||
|
||||
// Trigger the search button click again
|
||||
searchButton.click();
|
||||
}).catch(function(error) {
|
||||
console.error("reCAPTCHA error:", error);
|
||||
alert("reCAPTCHA verification failed. Please refresh the page.");
|
||||
});
|
||||
});
|
||||
}
|
||||
}, true); // Use capture phase to run before other click handlers
|
||||
});
|
||||
}'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Cloudflare Turnstile script and initialize
|
||||
*
|
||||
* @param string $site_key Turnstile site key
|
||||
* @param string $theme Theme (light, dark, auto)
|
||||
* @param string $size Size (normal, compact)
|
||||
* @return void
|
||||
*/
|
||||
private static function load_turnstile_script($site_key, $theme = 'auto', $size = 'normal') {
|
||||
// Enqueue Turnstile script
|
||||
wp_enqueue_script(
|
||||
'cloudflare-turnstile',
|
||||
'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback',
|
||||
[],
|
||||
'1.0',
|
||||
true
|
||||
);
|
||||
|
||||
// Pass settings to JavaScript
|
||||
wp_add_inline_script(
|
||||
'cloudflare-turnstile',
|
||||
'
|
||||
window.onloadTurnstileCallback = function() {
|
||||
console.log("[CAPTCHA Debug] Turnstile callback triggered");
|
||||
|
||||
// Store Turnstile settings globally
|
||||
window.checkerTurnstile = {
|
||||
siteKey: "' . esc_js($site_key) . '",
|
||||
theme: "' . esc_js($theme) . '",
|
||||
size: "' . esc_js($size) . '"
|
||||
};
|
||||
|
||||
console.log("[CAPTCHA Debug] Turnstile settings:", window.checkerTurnstile);
|
||||
|
||||
// Initialize Turnstile for all checker forms
|
||||
initTurnstileForForms();
|
||||
console.log("[CAPTCHA Debug] Turnstile initialization complete");
|
||||
};
|
||||
|
||||
// Also log when script loads
|
||||
console.log("[CAPTCHA Debug] Turnstile script loading...");
|
||||
|
||||
function initTurnstileForForms() {
|
||||
var forms = document.querySelectorAll(".dw-checker-container form");
|
||||
forms.forEach(function(form) {
|
||||
var submitBtn = form.querySelector(".search-button");
|
||||
|
||||
// Insert Turnstile widget before submit button
|
||||
var container = document.createElement("div");
|
||||
container.className = "dw-checker-turnstile-container";
|
||||
container.style.marginBottom = "1rem";
|
||||
|
||||
if (submitBtn) {
|
||||
submitBtn.parentNode.insertBefore(container, submitBtn);
|
||||
} else {
|
||||
form.appendChild(container);
|
||||
}
|
||||
|
||||
// Render Turnstile widget
|
||||
console.log("Turnstile: Initializing widget");
|
||||
turnstile.render(container, {
|
||||
sitekey: window.checkerTurnstile.siteKey,
|
||||
theme: window.checkerTurnstile.theme,
|
||||
size: window.checkerTurnstile.size,
|
||||
callback: function(token) {
|
||||
console.log("Turnstile: Token generated successfully");
|
||||
// Add token to hidden input
|
||||
var input = form.querySelector("input[name=turnstile_token]");
|
||||
if (!input) {
|
||||
input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = "turnstile_token";
|
||||
form.appendChild(input);
|
||||
}
|
||||
input.value = token;
|
||||
input.dataset.timestamp = Date.now().toString();
|
||||
console.log("Turnstile: Token stored in form");
|
||||
},
|
||||
"error-callback": function(error) {
|
||||
console.error("Turnstile error:", error);
|
||||
},
|
||||
"expired-callback": function() {
|
||||
console.warn("Turnstile: Token expired, please complete challenge again");
|
||||
var input = form.querySelector("input[name=turnstile_token]");
|
||||
if (input) {
|
||||
input.value = "";
|
||||
input.dataset.timestamp = "";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Modify form submit to ensure token is available
|
||||
form.addEventListener("submit", function(e) {
|
||||
var tokenInput = form.querySelector("input[name=turnstile_token]");
|
||||
if (!tokenInput || !tokenInput.value) {
|
||||
e.preventDefault();
|
||||
alert("Please wait for the security check to complete.");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CAPTCHA configuration for a checker
|
||||
*
|
||||
* @param int $checker_id Checker post ID
|
||||
* @return array Configuration data
|
||||
*/
|
||||
public static function get_captcha_config($checker_id) {
|
||||
$checker = get_post_meta($checker_id, 'checker', true);
|
||||
|
||||
if (!$checker) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$config = [
|
||||
'recaptcha' => [
|
||||
'enabled' => false,
|
||||
'site_key' => '',
|
||||
'action' => 'checker_validate',
|
||||
'hide_badge' => false
|
||||
],
|
||||
'turnstile' => [
|
||||
'enabled' => false,
|
||||
'site_key' => '',
|
||||
'theme' => 'auto',
|
||||
'size' => 'normal'
|
||||
]
|
||||
];
|
||||
|
||||
// Get reCAPTCHA settings
|
||||
if (isset($checker['security']['recaptcha']['enabled']) && $checker['security']['recaptcha']['enabled'] === 'yes') {
|
||||
$config['recaptcha'] = [
|
||||
'enabled' => true,
|
||||
'site_key' => $checker['security']['recaptcha']['site_key'] ?? '',
|
||||
'action' => $checker['security']['recaptcha']['action'] ?? 'checker_validate',
|
||||
'hide_badge' => $checker['security']['recaptcha']['hide_badge'] === 'yes'
|
||||
];
|
||||
}
|
||||
|
||||
// Get Turnstile settings
|
||||
if (isset($checker['security']['turnstile']['enabled']) && $checker['security']['turnstile']['enabled'] === 'yes') {
|
||||
$config['turnstile'] = [
|
||||
'enabled' => true,
|
||||
'site_key' => $checker['security']['turnstile']['site_key'] ?? '',
|
||||
'theme' => $checker['security']['turnstile']['theme'] ?? 'auto',
|
||||
'size' => $checker['security']['turnstile']['size'] ?? 'normal'
|
||||
];
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CAPTCHA field HTML for form
|
||||
*
|
||||
* @param int $checker_id Checker post ID
|
||||
* @return string HTML for CAPTCHA fields
|
||||
*/
|
||||
public static function get_captcha_fields($checker_id) {
|
||||
$config = self::get_captcha_config($checker_id);
|
||||
// Prefer Turnstile over reCAPTCHA when both are flagged (should not happen in UI)
|
||||
if ($config['turnstile']['enabled']) {
|
||||
$config['recaptcha']['enabled'] = false;
|
||||
}
|
||||
$html = '';
|
||||
|
||||
if ($config['recaptcha']['enabled'] || $config['turnstile']['enabled']) {
|
||||
// Add container for CAPTCHA
|
||||
$html .= '<div class="dw-checker-captcha-wrapper">';
|
||||
|
||||
if ($config['recaptcha']['enabled']) {
|
||||
// reCAPTCHA v3 doesn't need visible fields
|
||||
$html .= '<input type="hidden" name="recaptcha_token" id="recaptcha-token-' . esc_attr($checker_id) . '">';
|
||||
}
|
||||
|
||||
if ($config['turnstile']['enabled']) {
|
||||
// Turnstile container will be added dynamically
|
||||
$html .= '<div id="turnstile-container-' . esc_attr($checker_id) . '" class="dw-checker-turnstile-container"></div>';
|
||||
$html .= '<input type="hidden" name="turnstile_token" id="turnstile-token-' . esc_attr($checker_id) . '">';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if CAPTCHA is configured and valid
|
||||
*
|
||||
* @param int $checker_id Checker post ID
|
||||
* @return array Validity check result
|
||||
*/
|
||||
public static function validate_captcha_config($checker_id) {
|
||||
$config = self::get_captcha_config($checker_id);
|
||||
$result = [
|
||||
'valid' => false,
|
||||
'type' => null,
|
||||
'issues' => []
|
||||
];
|
||||
|
||||
// Check reCAPTCHA
|
||||
if ($config['recaptcha']['enabled']) {
|
||||
if (empty($config['recaptcha']['site_key'])) {
|
||||
$result['issues'][] = 'reCAPTCHA site key is missing';
|
||||
} elseif (!preg_match('/^6Lc[a-zA-Z0-9_-]{38}$/', $config['recaptcha']['site_key'])) {
|
||||
$result['issues'][] = 'reCAPTCHA site key appears to be invalid';
|
||||
}
|
||||
|
||||
if (count($result['issues']) === 0) {
|
||||
$result['valid'] = true;
|
||||
$result['type'] = 'recaptcha';
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Check Turnstile
|
||||
if ($config['turnstile']['enabled']) {
|
||||
if (empty($config['turnstile']['site_key'])) {
|
||||
$result['issues'][] = 'Turnstile site key is missing';
|
||||
} elseif (!preg_match('/^0x4AAA[a-zA-Z0-9_-]{33}$/', $config['turnstile']['site_key'])) {
|
||||
$result['issues'][] = 'Turnstile site key appears to be invalid';
|
||||
}
|
||||
|
||||
if (count($result['issues']) === 0) {
|
||||
$result['valid'] = true;
|
||||
$result['type'] = 'turnstile';
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// If both are enabled, it's an issue
|
||||
if ($config['recaptcha']['enabled'] && $config['turnstile']['enabled']) {
|
||||
$result['issues'][] = 'Both reCAPTCHA and Turnstile are enabled (only one should be used)';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
343
includes/logs/class-Security-Logger.php
Normal file
343
includes/logs/class-Security-Logger.php
Normal file
@@ -0,0 +1,343 @@
|
||||
<?php
|
||||
/**
|
||||
* Security Logger for Sheet Data Checker Pro
|
||||
*
|
||||
* Handles logging of security events including rate limit blocks,
|
||||
* CAPTCHA verification failures, and other security-related events
|
||||
*
|
||||
* @since 1.5.0
|
||||
*/
|
||||
|
||||
class CHECKER_SECURITY_LOGGER {
|
||||
|
||||
/**
|
||||
* Log a security event
|
||||
*
|
||||
* @param string $event_type Type of event (rate_limit, recaptcha, turnstile, nonce)
|
||||
* @param int $checker_id Checker post ID
|
||||
* @param array $event_data Event-specific data
|
||||
* @param string $level Log level (info, warning, error)
|
||||
* @return bool Success status
|
||||
*/
|
||||
public static function log_event($event_type, $checker_id, $event_data = [], $level = 'info') {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'checker_security_logs';
|
||||
|
||||
// Ensure table exists
|
||||
self::maybe_create_table();
|
||||
|
||||
// Prepare data
|
||||
$log_entry = [
|
||||
'event_type' => $event_type,
|
||||
'checker_id' => $checker_id,
|
||||
'ip_address' => self::mask_ip(self::get_client_ip()),
|
||||
'user_agent' => substr(sanitize_text_field($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255),
|
||||
'event_data' => json_encode($event_data),
|
||||
'level' => $level,
|
||||
'created_at' => current_time('mysql')
|
||||
];
|
||||
|
||||
// Insert log entry
|
||||
$result = $wpdb->insert($table_name, $log_entry);
|
||||
|
||||
// Also log to WordPress debug log if enabled
|
||||
if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) {
|
||||
$message = sprintf(
|
||||
'[Sheet Data Checker] Security Event: %s - Checker ID: %d - IP: %s - Data: %s',
|
||||
$event_type,
|
||||
$checker_id,
|
||||
self::mask_ip(self::get_client_ip()),
|
||||
json_encode($event_data)
|
||||
);
|
||||
|
||||
error_log($message);
|
||||
}
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log rate limit block
|
||||
*
|
||||
* @param int $checker_id Checker post ID
|
||||
* @param string $ip IP address
|
||||
* @param array $limit_config Rate limit configuration
|
||||
* @return bool Success status
|
||||
*/
|
||||
public static function log_rate_limit_block($checker_id, $ip, $limit_config) {
|
||||
return self::log_event(
|
||||
'rate_limit',
|
||||
$checker_id,
|
||||
[
|
||||
'ip' => $ip,
|
||||
'max_attempts' => $limit_config['max_attempts'] ?? 5,
|
||||
'time_window' => $limit_config['time_window'] ?? 15,
|
||||
'block_duration' => $limit_config['block_duration'] ?? 60
|
||||
],
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log CAPTCHA verification failure
|
||||
*
|
||||
* @param int $checker_id Checker post ID
|
||||
* @param string $captcha_type Type of CAPTCHA (recaptcha, turnstile)
|
||||
* @param array $verification_data Verification result data
|
||||
* @return bool Success status
|
||||
*/
|
||||
public static function log_captcha_failure($checker_id, $captcha_type, $verification_data) {
|
||||
return self::log_event(
|
||||
$captcha_type,
|
||||
$checker_id,
|
||||
[
|
||||
'success' => false,
|
||||
'score' => $verification_data['score'] ?? null,
|
||||
'error_codes' => $verification_data['error_codes'] ?? []
|
||||
],
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log nonce verification failure
|
||||
*
|
||||
* @param int $checker_id Checker post ID
|
||||
* @param string $nonce_value Nonce value that failed verification
|
||||
* @return bool Success status
|
||||
*/
|
||||
public static function log_nonce_failure($checker_id, $nonce_value) {
|
||||
return self::log_event(
|
||||
'nonce',
|
||||
$checker_id,
|
||||
[
|
||||
'nonce' => substr($nonce_value, 0, 10) . '...' // Only log first 10 chars for security
|
||||
],
|
||||
'error'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent security logs
|
||||
*
|
||||
* @param array $args Query arguments
|
||||
* @return array Log entries
|
||||
*/
|
||||
public static function get_logs($args = []) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'checker_security_logs';
|
||||
|
||||
// Default arguments
|
||||
$defaults = [
|
||||
'limit' => 50,
|
||||
'offset' => 0,
|
||||
'event_type' => null,
|
||||
'level' => null,
|
||||
'checker_id' => null,
|
||||
'date_from' => null,
|
||||
'date_to' => null
|
||||
];
|
||||
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
// Build WHERE clause
|
||||
$where = ['1=1'];
|
||||
$placeholders = [];
|
||||
|
||||
if ($args['event_type']) {
|
||||
$where[] = 'event_type = %s';
|
||||
$placeholders[] = $args['event_type'];
|
||||
}
|
||||
|
||||
if ($args['level']) {
|
||||
$where[] = 'level = %s';
|
||||
$placeholders[] = $args['level'];
|
||||
}
|
||||
|
||||
if ($args['checker_id']) {
|
||||
$where[] = 'checker_id = %d';
|
||||
$placeholders[] = $args['checker_id'];
|
||||
}
|
||||
|
||||
if ($args['date_from']) {
|
||||
$where[] = 'created_at >= %s';
|
||||
$placeholders[] = $args['date_from'];
|
||||
}
|
||||
|
||||
if ($args['date_to']) {
|
||||
$where[] = 'created_at <= %s';
|
||||
$placeholders[] = $args['date_to'];
|
||||
}
|
||||
|
||||
$where_clause = implode(' AND ', $where);
|
||||
|
||||
// Prepare query
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT * FROM $table_name
|
||||
WHERE $where_clause
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %d OFFSET %d",
|
||||
array_merge($placeholders, [$args['limit'], $args['offset']])
|
||||
);
|
||||
|
||||
return $wpdb->get_results($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get security statistics
|
||||
*
|
||||
* @param array $args Filter arguments
|
||||
* @return array Statistics
|
||||
*/
|
||||
public static function get_statistics($args = []) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'checker_security_logs';
|
||||
|
||||
// Default date range (last 30 days)
|
||||
$defaults = [
|
||||
'date_from' => date('Y-m-d', strtotime('-30 days')),
|
||||
'date_to' => date('Y-m-d')
|
||||
];
|
||||
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
// Prepare query
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
event_type,
|
||||
COUNT(*) as count,
|
||||
level
|
||||
FROM $table_name
|
||||
WHERE created_at BETWEEN %s AND %s
|
||||
GROUP BY event_type, level
|
||||
ORDER BY count DESC",
|
||||
[$args['date_from'], $args['date_to']]
|
||||
);
|
||||
|
||||
$results = $wpdb->get_results($query);
|
||||
|
||||
// Structure the results
|
||||
$stats = [
|
||||
'rate_limit' => ['total' => 0, 'warning' => 0, 'error' => 0],
|
||||
'recaptcha' => ['total' => 0, 'warning' => 0, 'error' => 0],
|
||||
'turnstile' => ['total' => 0, 'warning' => 0, 'error' => 0],
|
||||
'nonce' => ['total' => 0, 'warning' => 0, 'error' => 0],
|
||||
'total' => 0
|
||||
];
|
||||
|
||||
foreach ($results as $row) {
|
||||
if (isset($stats[$row->event_type])) {
|
||||
$stats[$row->event_type]['total'] += $row->count;
|
||||
$stats[$row->event_type][$row->level] = $row->count;
|
||||
$stats['total'] += $row->count;
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old log entries
|
||||
*
|
||||
* @param int $days Keep logs for this many days (default: 90)
|
||||
* @return int Number of rows deleted
|
||||
*/
|
||||
public static function cleanup_old_logs($days = 90) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'checker_security_logs';
|
||||
$cutoff_date = date('Y-m-d H:i:s', strtotime("-$days days"));
|
||||
|
||||
return $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM $table_name WHERE created_at < %s",
|
||||
$cutoff_date
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the security logs table if it doesn't exist
|
||||
*/
|
||||
private static function maybe_create_table() {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'checker_security_logs';
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
event_type varchar(50) NOT NULL,
|
||||
checker_id bigint(20) unsigned NOT NULL,
|
||||
ip_address varchar(45) NOT NULL,
|
||||
user_agent varchar(255) DEFAULT NULL,
|
||||
event_data longtext DEFAULT NULL,
|
||||
level varchar(10) NOT NULL DEFAULT 'info',
|
||||
created_at datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY event_type (event_type),
|
||||
KEY checker_id (checker_id),
|
||||
KEY created_at (created_at),
|
||||
KEY level (level)
|
||||
) $charset_collate;";
|
||||
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
dbDelta($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client IP address (reuses method from Security class)
|
||||
*
|
||||
* @return string IP address
|
||||
*/
|
||||
private static function get_client_ip() {
|
||||
// Check for Cloudflare first
|
||||
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
|
||||
return sanitize_text_field($_SERVER['HTTP_CF_CONNECTING_IP']);
|
||||
}
|
||||
|
||||
// Check various proxy headers
|
||||
$ip_headers = [
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_X_REAL_IP',
|
||||
'HTTP_X_FORWARDED',
|
||||
'HTTP_FORWARDED_FOR',
|
||||
'HTTP_FORWARDED',
|
||||
'REMOTE_ADDR'
|
||||
];
|
||||
|
||||
foreach ($ip_headers as $header) {
|
||||
if (!empty($_SERVER[$header])) {
|
||||
$ips = explode(',', $_SERVER[$header]);
|
||||
$ip = trim($ips[0]);
|
||||
|
||||
// Validate IP
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !empty($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : '0.0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mask IP address for privacy
|
||||
*
|
||||
* @param string $ip IP address
|
||||
* @return string Masked IP address
|
||||
*/
|
||||
private static function mask_ip($ip) {
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$parts = explode('.', $ip);
|
||||
return $parts[0] . '.' . $parts[1] . '.***.***';
|
||||
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$parts = explode(':', $ip);
|
||||
return $parts[0] . ':' . $parts[1] . '::***';
|
||||
}
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user