<?php
/**
 * Web Application Firewall (WAF) for BotFend Anti-Bot Firewall
 *
 * @package BotFend\Security
 * @since 4.4.1 - Patched object cache sync and SQL escaping
 * @version 4.8.3 - Rebranded from SafeBot, external rules disabled by default
 */

namespace BotFend\Security;

use BotFend\Core\Config;
use BotFend\Utilities\Utilities;
use BotFend\Database\Database;

defined('ABSPATH') || exit;

/**
 * Class WAF
 *
 * Web Application Firewall protecting against SQL injection, XSS, and other attacks
 */
class WAF {
    
    private $config;
    private $utilities;
    private $database;
    private $waf_rules = [];
    private $waf_enabled = true;
    private $block_threshold = 15;
    private $action_mode = 'score';
    private $integrate_scoring = true;
    private $scan_start_time;
    private $max_scan_time = 0.5;
    private $rate_limit = 10;
    
    const MAX_REQUEST_SIZE = 1048576;
    const MAX_SCAN_DEPTH = 5;
    const FALSE_POSITIVE_TIMEOUT = 300;
    const MAX_CRAWLER_CACHE = 100;
    const MAX_FP_CACHE = 500;
    
    private static $crawler_cache = [];
    private static $false_positive_cache = [];
    private static $validated_rules = [];
    
    private $whitelisted_paths = [
        '/wp-admin/admin-ajax.php',
        '/wp-admin/admin-post.php',
        '/wp-cron.php',
    ];
    
    private $whitelisted_params = [
        'post_content',
        'post_excerpt',
        'comment_content',
        'g-recaptcha-response',
        '_wpnonce',
    ];
    
    private $context_rules = [
        'admin' => ['file_inclusion'],
        'ajax' => ['command_injection'],
        'rest' => [],
        'login' => ['command_injection'],
        'search' => [],
    ];
    
    private $confidence_scores = [
        'sql_injection' => 0.9,
        'xss' => 0.8,
        'command_injection' => 0.95,
        'path_traversal' => 0.85,
        'wp_vulnerabilities' => 0.9,
    ];
    
    public function __construct(Utilities $utilities, Database $database) {
        $this->config = Config::get_instance();
        $this->utilities = $utilities;
        $this->database = $database;
        
        $this->waf_enabled = $this->config->get_bool('enable_waf', true);
        $this->block_threshold = $this->config->get_int('waf_block_threshold', 15);
        $this->action_mode = $this->config->get_string('waf_action_mode', 'score');
        $this->integrate_scoring = $this->config->get_bool('waf_integrate_scoring', true);
        $this->max_scan_time = $this->config->get_float('waf_max_scan_time', 0.5);
        $this->rate_limit = $this->config->get_int('waf_rate_limit', 10);
        
        if ($this->waf_enabled) {
            $this->load_waf_rules();
            add_action('init', [$this, 'scan_request'], 5);
        }
    }
    
    private function load_waf_rules() {
        $cache_key = 'botfend_waf_rules_' . md5(BOTFEND_VERSION);
        $cached_rules = wp_cache_get($cache_key, 'botfend_defender');
        
        if (false !== $cached_rules) {
            $this->waf_rules = $cached_rules;
            return;
        }
        
        $this->waf_rules = [
            'sql_injection' => [
                'name' => 'SQL Injection',
                'patterns' => $this->get_sql_patterns(),
                'score' => 8,
                'priority' => 10,
                'description' => esc_html__('Attempts to inject SQL commands', 'botfend-anti-bot-firewall')
            ],
            'xss' => [
                'name' => 'XSS Attack',
                'patterns' => $this->get_xss_patterns(),
                'score' => 6,
                'priority' => 9,
                'description' => esc_html__('Attempts to inject malicious scripts', 'botfend-anti-bot-firewall')
            ],
            'command_injection' => [
                'name' => 'Command Injection',
                'patterns' => $this->get_command_patterns(),
                'score' => 8,
                'priority' => 8,
                'description' => esc_html__('Attempts to execute system commands', 'botfend-anti-bot-firewall')
            ],
            'path_traversal' => [
                'name' => 'Path Traversal',
                'patterns' => $this->get_path_patterns(),
                'score' => 5,
                'priority' => 7,
                'description' => esc_html__('Attempts to access unauthorized directories', 'botfend-anti-bot-firewall')
            ],
        ];
        
        // WordPress vulnerability rules are DISABLED BY DEFAULT (opt-in required)
        if ($this->config->get_bool('waf_wp_rules', false)) {
            $this->waf_rules['wp_vulnerabilities'] = [
                'name' => 'WordPress Vulnerabilities',
                'patterns' => $this->get_wp_patterns(),
                'score' => 7,
                'priority' => 6,
                'description' => esc_html__('Attempts to exploit WordPress vulnerabilities', 'botfend-anti-bot-firewall')
            ];
        }
        
        $this->validate_all_patterns();
        
        wp_cache_set($cache_key, $this->waf_rules, 'botfend_defender', HOUR_IN_SECONDS);
    }
    
    private function get_sql_patterns() {
        return [
            '/(union\s+select\s+.*\s+from)/i',
            '/(select\s+.*\s+from\s+.*\s+where)/i',
            '/(insert\s+into\s+.*\s+values)/i',
            '/(update\s+.*\s+set\s+.*\s+where)/i',
            '/(delete\s+from\s+.*\s+where)/i',
            '/(drop\s+table\s+)/i',
            '/(create\s+table\s+)/i',
            '/(alter\s+table\s+)/i',
            '/(truncate\s+table\s+)/i',
            '/(benchmark\s*\(|sleep\s*\(|waitfor\s+delay)/i',
            '/\/\*.*\*\//',
        ];
    }
    
    private function get_xss_patterns() {
        return [
            '/<script[^>]*>.*<\/script[^>]*>/is',
            '/javascript\s*:/i',
            '/(onload|onerror|onclick|onmouseover|onmouseout|onfocus|onblur)\s*=\s*["\'][^"\']*["\']/i',
            '/(alert|confirm|prompt)\s*\(/i',
            '/document\.(cookie|write|location)/i',
            '/text\/html/i',
            '/vbscript\s*:/i',
        ];
    }
    
    private function get_command_patterns() {
        return [
            '/(system|exec|shell_exec|passthru)\s*\(/i',
            '/`.*`/',
            '/\|\s*(sh|bash|cmd|powershell|php)/i',
            '/&&\s*(wget|curl|rm|mv|cp|chmod|chown)/i',
            '/\$\s*\(/',
        ];
    }
    
    private function get_path_patterns() {
        return [
            '/(\.\.\/){3,}/',
            '/(\.\.\\\\){3,}/',
            '/%2e%2e%2f/i',
            '/%2e%2e%5c/i',
            '/%2e%2e%252f/i',
            '/%00/',
            '/\\x00/',
        ];
    }
    
    private function get_wp_patterns() {
        return [
            '/wp-config\.php/i',
            '/\.env(\.[^\.]*)?$/i',
            '/\.git\/config/i',
            '/\.svn\/entries/i',
            '/phpinfo\.php/i',
            '/\/uploads\/.*\.php(\?|$)/i',
            '/(backup|backup_|backup-\d+)\.sql/i',
        ];
    }
    
    private function validate_all_patterns() {
        foreach ($this->waf_rules as $category => &$rule) {
            $valid_patterns = [];
            foreach ($rule['patterns'] as $index => $pattern) {
                $pattern_hash = md5($pattern . $category);
                if (isset(self::$validated_rules[$category][$pattern_hash])) {
                    $valid_patterns[] = $pattern;
                    continue;
                }
                
                $test_result = @preg_match($pattern, '');
                
                if ($test_result !== false) {
                    $valid_patterns[] = $pattern;
                    self::$validated_rules[$category][$pattern_hash] = true;
                } else {
                    $error = error_get_last();
                    error_log(sprintf(
                        'BotFend WAF: Invalid pattern in %s (index %d): %s - Error: %s',
                        $category,
                        $index,
                        substr($pattern, 0, 50) . (strlen($pattern) > 50 ? '...' : ''),
                        sanitize_text_field($error['message'] ?? 'Unknown error')
                    ));
                }
            }
            $rule['patterns'] = $valid_patterns;
        }
    }
    
    private function should_skip_request($ip, $user_agent, $request_uri) {
        if ($this->utilities->is_ip_whitelisted($ip)) {
            return true;
        }
        
        foreach ($this->whitelisted_paths as $path) {
            if (strpos($request_uri, $path) !== false) {
                return true;
            }
        }
        
        if (is_user_logged_in()) {
            $current_user = wp_get_current_user();
            if ($current_user->exists() && $current_user->has_cap('edit_posts')) {
                return true;
            }
        }
        
        if ($this->is_legitimate_crawler($ip, $user_agent)) {
            return true;
        }
        
        return false;
    }
    
    private function is_legitimate_crawler($ip, $user_agent) {
        if (empty($user_agent)) {
            return false;
        }
        
        $cache_key = md5($ip . $user_agent);
        if (isset(self::$crawler_cache[$cache_key])) {
            return self::$crawler_cache[$cache_key];
        }
        
        $result = false;
        if ($this->utilities && method_exists($this->utilities, 'verify_legitimate_crawler')) {
            $result = $this->utilities->verify_legitimate_crawler($ip, $user_agent);
        }
        
        if (count(self::$crawler_cache) > self::MAX_CRAWLER_CACHE) {
            self::$crawler_cache = array_slice(self::$crawler_cache, -50, null, true);
        }
        
        self::$crawler_cache[$cache_key] = $result;
        return $result;
    }
    
    private function get_disabled_rules_for_context() {
        $disabled = [];
        
        if (is_admin()) {
            $disabled = array_merge($disabled, $this->context_rules['admin'] ?? []);
        }
        
        if (wp_doing_ajax()) {
            $disabled = array_merge($disabled, $this->context_rules['ajax'] ?? []);
        }
        
        if (defined('REST_REQUEST') && REST_REQUEST) {
            $disabled = array_merge($disabled, $this->context_rules['rest'] ?? []);
        }
        
        $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
        if (strpos($request_uri, 'wp-login.php') !== false) {
            $disabled = array_merge($disabled, $this->context_rules['login'] ?? []);
        }
        
        return array_unique($disabled);
    }
    
    private function should_skip_parameter($key, $value) {
        $key_lower = strtolower($key);
        foreach ($this->whitelisted_params as $skip_param) {
            if (strpos($key_lower, $skip_param) !== false) {
                return true;
            }
        }
        
        if ($key === '_wpnonce' && preg_match('/^[a-f0-9]{10,}$/i', $value)) {
            return true;
        }
        
        if (in_array($key, ['post_id', 'page_id', 'cat', 'tag_id', 'author'], true) && 
            is_numeric($value)) {
            return true;
        }
        
        return false;
    }
    
    private function scan_request_data($request_data) {
        $total_score = 0;
        $detections = [];
        $disabled_rules = $this->get_disabled_rules_for_context();
        $this->scan_start_time = microtime(true);
        
        uasort($this->waf_rules, function($a, $b) {
            return $b['priority'] - $a['priority'];
        });
        
        foreach ($this->waf_rules as $category => $rule) {
            if (in_array($category, $disabled_rules, true)) {
                continue;
            }
            
            if (microtime(true) - $this->scan_start_time > $this->max_scan_time) {
                break;
            }
            
            $category_score = 0;
            $category_detections = [];
            $confidence = $this->confidence_scores[$category] ?? 0.8;
            
            foreach ($rule['patterns'] as $pattern) {
                $found = $this->scan_pattern_with_context($pattern, $request_data, $category);
                
                if ($found) {
                    $pattern_score = (int)($rule['score'] * $confidence);
                    $category_score += $pattern_score;
                    $category_detections[] = $rule['name'];
                    
                    if ($category_score >= $this->block_threshold) {
                        break;
                    }
                }
            }
            
            if ($category_score > 0) {
                $total_score += $category_score;
                $detections = array_merge($detections, $category_detections);
                
                if ($total_score >= $this->block_threshold) {
                    break;
                }
            }
        }
        
        if (count(array_unique($detections)) > 1) {
            $total_score = min(15, (int)($total_score * 1.3));
        }
        
        return [
            'score' => min(15, $total_score),
            'detections' => array_unique($detections)
        ];
    }
    
    private function scan_pattern_with_context($pattern, $data, $rule_category = '', $prefix = '', $depth = 0) {
        if ($depth > self::MAX_SCAN_DEPTH) {
            return false;
        }
        
        foreach ($data as $key => $value) {
            $current_key = $prefix ? $prefix . '[' . $key . ']' : $key;
            
            if ($this->should_skip_parameter($key, $value)) {
                continue;
            }
            
            if (is_array($value)) {
                if ($this->scan_pattern_with_context($pattern, $value, $rule_category, $current_key, $depth + 1)) {
                    return true;
                }
            } elseif (is_string($value)) {
                if ($this->is_likely_false_positive($value, $pattern, $rule_category, $current_key)) {
                    continue;
                }
                
                if (@preg_match($pattern, $value)) {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    private function is_likely_false_positive($value, $pattern, $rule_category, $key) {
        $cache_key = md5($value . $pattern . $key . $rule_category);
        if (isset(self::$false_positive_cache[$cache_key])) {
            return self::$false_positive_cache[$cache_key];
        }
        
        if (count(self::$false_positive_cache) > self::MAX_FP_CACHE) {
            self::$false_positive_cache = array_slice(self::$false_positive_cache, -100, null, true);
        }
        
        $value_lower = strtolower($value);
        
        if (preg_match('/^(a|s|O|i|d):\d+:/', $value)) {
            self::$false_positive_cache[$cache_key] = true;
            return true;
        }
        
        switch ($rule_category) {
            case 'sql_injection':
                if (preg_match('/\b(select|union|insert|update|delete)\b.*\b(in|from|where)\b/i', $value)) {
                    $word_count = str_word_count($value_lower);
                    if ($word_count > 10) {
                        self::$false_positive_cache[$cache_key] = true;
                        return true;
                    }
                }
                break;
                
            case 'xss':
                if (strpos($value_lower, '<script') !== false) {
                    if (preg_match('/example|tutorial|documentation|how to|learn/i', $value_lower)) {
                        self::$false_positive_cache[$cache_key] = true;
                        return true;
                    }
                }
                break;
                
            case 'command_injection':
                if (preg_match('/\b(exec|system|shell_exec)\b/i', $value_lower)) {
                    if (preg_match('/function|method|example|tutorial/i', $value_lower)) {
                        self::$false_positive_cache[$cache_key] = true;
                        return true;
                    }
                }
                break;
                
            case 'path_traversal':
                if (strpos($key, 'url') !== false || strpos($key, 'link') !== false) {
                    if (preg_match('/^(https?|ftp):\/\//', $value)) {
                        self::$false_positive_cache[$cache_key] = true;
                        return true;
                    }
                }
                break;
        }
        
        if ($this->is_json($value)) {
            $decoded = json_decode($value, true);
            if (is_array($decoded)) {
                $json_value = implode(' ', array_filter((array)$decoded, 'is_string'));
                if (strlen($json_value) > 0) {
                    if (@preg_match($pattern, $json_value)) {
                        self::$false_positive_cache[$cache_key] = false;
                        return false;
                    }
                }
                self::$false_positive_cache[$cache_key] = true;
                return true;
            }
        }
        
        if (strpos($value, 'image/') === 0 && 
            strlen($value) > 100 && 
            preg_match('/^[a-zA-Z0-9\/+]+={0,2}$/', $value)) {
            self::$false_positive_cache[$cache_key] = true;
            return true;
        }
        
        if (strlen($value) > 1000) {
            if (@preg_match($pattern, $value)) {
                self::$false_positive_cache[$cache_key] = false;
                return false;
            }
            self::$false_positive_cache[$cache_key] = true;
            return true;
        }
        
        self::$false_positive_cache[$cache_key] = false;
        return false;
    }
    
    private function is_json($string) {
        if (!is_string($string) || empty($string)) {
            return false;
        }
        
        json_decode($string);
        return json_last_error() === JSON_ERROR_NONE;
    }
    
    public function scan_request() {
        if (!$this->waf_enabled) {
            return;
        }
        
        $this->scan_start_time = microtime(true);
        
        $ip = $this->utilities->get_client_ip();
        $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? 
            sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
        
        $safe_request_uri = isset($_SERVER['REQUEST_URI']) ? 
            esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : '';
        
        if ($this->should_skip_request($ip, $user_agent, $safe_request_uri)) {
            return;
        }
        
        $is_rate_limited = !$this->check_rate_limit($ip);
        
        $request_data = $this->collect_request_data();
        $request_data['raw_uri'] = $safe_request_uri;
        
        $waf_result = $this->scan_request_data($request_data);
        
        if ($waf_result['score'] > 0 && !$is_rate_limited) {
            $this->apply_waf_action($ip, $waf_result);
        } elseif ($waf_result['score'] > 3 && $is_rate_limited) {
            $this->log_detection_only($ip, $waf_result);
        }
    }
    
    private function collect_request_data() {
        $data = [
            'get' => [],
            'post' => [],
            'request_uri' => isset($_SERVER['REQUEST_URI']) ? 
                           esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : '',
        ];
        
        if (!empty($_GET)) {
            foreach ($_GET as $key => $value) {
                if (is_string($key) && is_scalar($value)) {
                    $key = sanitize_text_field($key);
                    $value = sanitize_text_field($value);
                    $data['get'][$key] = $value;
                }
            }
        }
        
        if (!empty($_POST) && strlen(serialize($_POST)) < self::MAX_REQUEST_SIZE) {
            foreach ($_POST as $key => $value) {
                if (is_string($key) && is_scalar($value)) {
                    $key = sanitize_text_field($key);
                    $value = sanitize_text_field($value);
                    $data['post'][$key] = $value;
                }
            }
        }
        
        return $data;
    }
    
    private function log_detection_only($ip, $waf_result) {
        $score = $waf_result['score'];
        $detections = $waf_result['detections'];
        $country = $this->utilities->get_country($ip);
        $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? 
                     sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
        
        $log_factors = [
            sprintf(
                esc_html__('WAF (Rate Limited): %s (Score: %d)', 'botfend-anti-bot-firewall'),
                esc_html(implode(', ', array_slice($detections, 0, 3))),
                absint($score)
            )
        ];
        
        $this->database->log_request(
            sanitize_text_field($ip),
            absint($score),
            'waf_detection_limited',
            $log_factors,
            sanitize_text_field($user_agent),
            false,
            sanitize_text_field($country)
        );
    }
    
    private function apply_waf_action($ip, $waf_result) {
        $score = $waf_result['score'];
        $detections = $waf_result['detections'];
        $country = $this->utilities->get_country($ip);
        $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? 
                     sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
        
        $log_factors = [
            sprintf(
                esc_html__('WAF: %s (Score: %d)', 'botfend-anti-bot-firewall'),
                esc_html(implode(', ', array_slice($detections, 0, 3))),
                absint($score)
            )
        ];
        
        $this->database->log_request(
            sanitize_text_field($ip),
            absint($score),
            'waf_detection',
            $log_factors,
            sanitize_text_field($user_agent),
            false,
            sanitize_text_field($country)
        );
        
        switch ($this->action_mode) {
            case 'log_only':
                return;
                
            case 'challenge':
                if ($score >= $this->block_threshold) {
                    $this->block_ip($ip, $score, $detections, $country);
                } elseif ($score >= 5) {
                    $this->issue_challenge($ip);
                }
                break;
                
            case 'block':
                if ($score >= $this->block_threshold) {
                    $this->block_ip($ip, $score, $detections, $country);
                }
                break;
                
            case 'score':
            default:
                if ($this->integrate_scoring) {
                    $this->add_to_bot_score($ip, $score);
                }
                break;
        }
    }
    
    private function add_to_bot_score($ip, $score) {
        $score_key = 'botfend_waf_score_' . md5($ip);
        $current_score = absint(get_transient($score_key) ?: 0);
        $new_score = $current_score + $score;
        $new_score = min($new_score, 10);
        
        set_transient($score_key, $new_score, HOUR_IN_SECONDS);
    }
    
    private function block_ip($ip, $score, $detections, $country) {
        $block_duration = $this->config->get_int('block_duration', 24);
        
        $this->database->block_ip(
            sanitize_text_field($ip),
            'waf_block',
            sprintf(
                esc_html__('WAF Block: %s', 'botfend-anti-bot-firewall'),
                esc_html(implode(', ', array_slice($detections, 0, 3)))
            ),
            absint($score),
            absint($block_duration),
            sanitize_text_field($country)
        );
        
        $block_flag_key = 'botfend_waf_immediate_block_' . md5($ip);
        set_transient($block_flag_key, true, 300);
    }
    
    private function issue_challenge($ip) {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log("BotFend WAF: Would issue challenge for IP " . sanitize_text_field($ip));
        }
    }
    
    private function check_rate_limit($ip) {
        $cache_key = 'botfend_waf_rate_' . md5($ip);
        $attempts = (int) get_transient($cache_key);
        
        if ($attempts >= $this->rate_limit) {
            return false;
        }
        
        $attempts++;
        set_transient($cache_key, $attempts, 60);
        return true;
    }
    
    public function clear_cache() {
        global $wpdb;
        
        // Safe query format with Object Cache Check and Escaped Wildcards
        if (!wp_using_ext_object_cache()) {
            $like_str = $wpdb->esc_like('_transient_botfend_waf_rate_') . '%';
            $keys = $wpdb->get_col($wpdb->prepare(
                "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
                $like_str
            ));
            
            if (!empty($keys)) {
                foreach ($keys as $key) {
                    $transient_name = str_replace('_transient_', '', $key);
                    delete_transient($transient_name);
                }
            }
        }
        
        $cache_key = 'botfend_waf_rules_' . md5(BOTFEND_VERSION);
        wp_cache_delete($cache_key, 'botfend_defender');
        
        if (function_exists('wp_cache_flush_group')) {
            wp_cache_flush_group('botfend_defender');
        }
        
        self::$validated_rules = [];
        self::$false_positive_cache = [];
        self::$crawler_cache = [];
    }
    
    public function get_status() {
        return [
            'enabled' => $this->waf_enabled,
            'action_mode' => $this->action_mode,
            'block_threshold' => $this->block_threshold,
            'integrate_scoring' => $this->integrate_scoring,
            'max_scan_time' => $this->max_scan_time,
            'rate_limit' => $this->rate_limit,
            'rules_count' => count($this->waf_rules),
            'enabled_rules' => array_keys($this->waf_rules),
            'whitelisted_paths' => $this->whitelisted_paths,
            'whitelisted_params' => $this->whitelisted_params,
        ];
    }
}