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' => ''
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user