get_option('woonoow_enable_checkout_rate_limit', 'yes') === 'yes', 'rate_limit_orders' => intval(get_option('woonoow_rate_limit_orders', 5)), 'rate_limit_minutes' => intval(get_option('woonoow_rate_limit_minutes', 10)), // CAPTCHA 'captcha_provider' => get_option('woonoow_captcha_provider', 'none'), // none, recaptcha, turnstile 'recaptcha_site_key' => get_option('woonoow_recaptcha_site_key', ''), 'recaptcha_secret_key' => get_option('woonoow_recaptcha_secret_key', ''), 'turnstile_site_key' => get_option('woonoow_turnstile_site_key', ''), 'turnstile_secret_key' => get_option('woonoow_turnstile_secret_key', ''), ]; } /** * Get public settings (safe to expose to frontend) * * @return array */ public static function get_public_settings() { $settings = self::get_settings(); return [ 'captcha_provider' => $settings['captcha_provider'], 'recaptcha_site_key' => $settings['recaptcha_site_key'], 'turnstile_site_key' => $settings['turnstile_site_key'], ]; } /** * Update security settings * * @param array $settings * @return bool */ public static function update_settings($settings) { // Rate Limiting if (array_key_exists('enable_checkout_rate_limit', $settings)) { $value = !empty($settings['enable_checkout_rate_limit']) ? 'yes' : 'no'; update_option('woonoow_enable_checkout_rate_limit', $value); } if (isset($settings['rate_limit_orders'])) { $value = max(1, intval($settings['rate_limit_orders'])); update_option('woonoow_rate_limit_orders', $value); } if (isset($settings['rate_limit_minutes'])) { $value = max(1, intval($settings['rate_limit_minutes'])); update_option('woonoow_rate_limit_minutes', $value); } // CAPTCHA Provider if (isset($settings['captcha_provider'])) { $valid_providers = ['none', 'recaptcha', 'turnstile']; $value = in_array($settings['captcha_provider'], $valid_providers) ? $settings['captcha_provider'] : 'none'; update_option('woonoow_captcha_provider', $value); } // reCAPTCHA Keys if (isset($settings['recaptcha_site_key'])) { update_option('woonoow_recaptcha_site_key', sanitize_text_field($settings['recaptcha_site_key'])); } if (isset($settings['recaptcha_secret_key'])) { update_option('woonoow_recaptcha_secret_key', sanitize_text_field($settings['recaptcha_secret_key'])); } // Turnstile Keys if (isset($settings['turnstile_site_key'])) { update_option('woonoow_turnstile_site_key', sanitize_text_field($settings['turnstile_site_key'])); } if (isset($settings['turnstile_secret_key'])) { update_option('woonoow_turnstile_secret_key', sanitize_text_field($settings['turnstile_secret_key'])); } return true; } /** * Check if rate limit is exceeded for an IP * * @param string|null $ip IP address (null = auto-detect) * @return bool True if rate limit exceeded */ public static function is_rate_limited($ip = null) { $settings = self::get_settings(); if (!$settings['enable_checkout_rate_limit']) { return false; } if ($ip === null) { $ip = self::get_client_ip(); } $transient_key = 'woonoow_rate_' . md5($ip); $attempts = get_transient($transient_key); if ($attempts === false) { return false; } return intval($attempts) >= $settings['rate_limit_orders']; } /** * Record an order attempt for rate limiting * * @param string|null $ip IP address (null = auto-detect) * @return void */ public static function record_order_attempt($ip = null) { $settings = self::get_settings(); if (!$settings['enable_checkout_rate_limit']) { return; } if ($ip === null) { $ip = self::get_client_ip(); } $transient_key = 'woonoow_rate_' . md5($ip); $attempts = get_transient($transient_key); if ($attempts === false) { // First attempt, set with expiration set_transient($transient_key, 1, $settings['rate_limit_minutes'] * MINUTE_IN_SECONDS); } else { // Increment attempts (keep same expiration by getting remaining time) $attempts = intval($attempts) + 1; set_transient($transient_key, $attempts, $settings['rate_limit_minutes'] * MINUTE_IN_SECONDS); } } /** * Validate CAPTCHA token * * @param string $token CAPTCHA token from frontend * @return bool|WP_Error True if valid, WP_Error if invalid */ public static function validate_captcha($token) { $settings = self::get_settings(); if ($settings['captcha_provider'] === 'none') { return true; // No CAPTCHA enabled } if (empty($token)) { return new \WP_Error('captcha_missing', __('CAPTCHA verification required', 'woonoow')); } if ($settings['captcha_provider'] === 'recaptcha') { return self::validate_recaptcha($token, $settings['recaptcha_secret_key']); } if ($settings['captcha_provider'] === 'turnstile') { return self::validate_turnstile($token, $settings['turnstile_secret_key']); } return true; } /** * Validate Google reCAPTCHA v3 token * * @param string $token Token from frontend * @param string $secret_key Secret key * @return bool|WP_Error */ private static function validate_recaptcha($token, $secret_key) { if (empty($secret_key)) { return new \WP_Error('captcha_config', __('reCAPTCHA not configured', 'woonoow')); } $response = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', [ 'body' => [ 'secret' => $secret_key, 'response' => $token, 'remoteip' => self::get_client_ip(), ], 'timeout' => 10, ]); if (is_wp_error($response)) { return new \WP_Error('captcha_error', __('CAPTCHA verification failed', 'woonoow')); } $body = json_decode(wp_remote_retrieve_body($response), true); if (!isset($body['success']) || !$body['success']) { return new \WP_Error('captcha_invalid', __('CAPTCHA verification failed', 'woonoow')); } // reCAPTCHA v3 returns a score (0.0 - 1.0), we accept 0.5 and above $score = $body['score'] ?? 0; if ($score < 0.5) { return new \WP_Error('captcha_score', __('CAPTCHA score too low', 'woonoow')); } return true; } /** * Validate Cloudflare Turnstile token * * @param string $token Token from frontend * @param string $secret_key Secret key * @return bool|WP_Error */ private static function validate_turnstile($token, $secret_key) { if (empty($secret_key)) { return new \WP_Error('captcha_config', __('Turnstile not configured', 'woonoow')); } $response = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [ 'body' => [ 'secret' => $secret_key, 'response' => $token, 'remoteip' => self::get_client_ip(), ], 'timeout' => 10, ]); if (is_wp_error($response)) { return new \WP_Error('captcha_error', __('CAPTCHA verification failed', 'woonoow')); } $body = json_decode(wp_remote_retrieve_body($response), true); if (!isset($body['success']) || !$body['success']) { return new \WP_Error('captcha_invalid', __('CAPTCHA verification failed', 'woonoow')); } return true; } /** * Get client IP address * * @return string */ private static function get_client_ip() { $headers = [ 'HTTP_CF_CONNECTING_IP', // Cloudflare 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR', ]; foreach ($headers as $header) { if (!empty($_SERVER[$header])) { $ip = $_SERVER[$header]; // Handle comma-separated list (X-Forwarded-For) if (strpos($ip, ',') !== false) { $ip = trim(explode(',', $ip)[0]); } if (filter_var($ip, FILTER_VALIDATE_IP)) { return $ip; } } } return '127.0.0.1'; } }