Files
WooNooW/includes/Compat/SecuritySettingsProvider.php

302 lines
9.2 KiB
PHP

<?php
/**
* Security Settings Provider
*
* Provides security-related settings including rate limiting and CAPTCHA.
*
* @package WooNooW
*/
namespace WooNooW\Compat;
class SecuritySettingsProvider
{
/**
* Get security settings
*
* @return array
*/
public static function get_settings()
{
return [
// Rate Limiting
'enable_checkout_rate_limit' => 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';
}
}