<?php
/**
 * User Option Audit Scanner
 *
 * @package VMPFence
 * @since 1.0.0
 * @since 2.2.4 -- SQL Query fix
 */

//Prevent Direct Access
if(!defined('ABSPATH')){
    exit;
}

class VMPFence_User_Option_Audit_Scanner {
    
    private $debug_log = array();
    private $scan_options = null;
    
    
    // This scanner focuses exclusively on WordPress options
    
    public function __construct() {
        $this->debug_log[] = 'WordPress Options Audit Scanner initialized';
    }
    
    /**
     * Check if an option should be skipped from scanning
     */
    private function should_skip_option($option_name) {
        // Skip VMPFence's own options to prevent false positives
        $vmpfence_options = array(
            'vmpfence_config',
            'vmpfence_scan_config', 
            'vmpfence_last_scan_data',
            'vmpfence_debug_logs_server_state',
            'vmpfence_debug_logs_file_changes',
            'vmpfence_debug_logs_user_security',
            'vmpfence_debug_logs_content_safety',
            'vmpfence_debug_logs_public_files',
            'vmpfence_debug_logs_user_audit',
            'vmpfence_debug_logs_vulnerability',
            'vmpfence_debug_logs_malware',
            'vmpfence_last_scan_debug_logs',
            'vmpfence_scan_statistics',
            'vmpfence_exclusions',
            'vmpfence_settings'
        );
        
        // Skip if it's a VMPFence option
        if (in_array($option_name, $vmpfence_options) || strpos($option_name, 'vmpfence_') === 0) {
            return true;
        }
        
        // Skip other common WordPress options that might contain code references but are legitimate
        $safe_prefixes = array(
            '_transient_',           // WordPress transients
            '_site_transient_',      // Site transients
            'widget_',               // Widget data
            'sidebars_widgets',      // Sidebar configuration
            'cron',                  // WordPress cron jobs
            'rewrite_rules',         // URL rewrite rules
            'auto_updater.',         // WordPress auto-updater data
        );
        
        foreach ($safe_prefixes as $safe_prefix) {
            if (strpos($option_name, $safe_prefix) === 0) {
                return true;
            }
        }
        
        // Skip specific WordPress core options that may contain legitimate code references
        $safe_core_options = array(
            'active_plugins',
            'stylesheet', 
            'template',
            'wp_user_roles',
            'can_compress_scripts',
            'recently_activated',
            'uninstall_plugins'
        );
        
        if (in_array($option_name, $safe_core_options)) {
            return true;
        }
        
        return false;
    }
    
    public function scan($scan_options = null) {
        // Initialize performance controls if available
        $use_performance_controls = false;
        if (class_exists('VMPFence_Performance_Manager')) {
            VMPFence_Performance_Manager::init();
            $use_performance_controls = true;
            $this->debug_log[] = 'User Option Audit Scanner: Performance controls initialized';
        }
        
        // If scan options provided, respect them
        if ($scan_options) {
            $this->scan_options = $scan_options;
            $this->debug_log[] = 'User Option Audit Scanner: Scan options loaded';
        }
        
        $this->debug_log[] = 'Starting User & Option Audit scan...';
        
        $issues = array();
        $scanned_count = 0;
        
        try {
            // Clean up old user audit issues before scanning
            $this->cleanup_old_issues();
            
            // 1. Suspicious Options Scan - Check WordPress options for malicious URLs
            if ($this->should_run_scan('scansEnabled_suspiciousOptions')) {
                $option_issues = $this->scan_suspicious_options();
                $issues = array_merge($issues, $option_issues);
                $scanned_count += $this->get_scanned_options_count();
                $this->debug_log[] = 'Suspicious options scan completed';
            } else {
                $this->debug_log[] = 'Suspicious options scan disabled by configuration';
            }
            
            // NOTE: Admin user scanning is handled by VMPFence_User_Security_Scanner
            // This scanner only handles WordPress options scanning
            
            // Always store scan results in database (even if no issues found)
            $this->store_issues($issues, $scanned_count);
            
            $this->debug_log[] = "User & Option Audit scan completed. Found " . count($issues) . " issues from {$scanned_count} items checked.";
            
        } catch (Exception $e) {
            $this->debug_log[] = 'Error during User & Option Audit scan: ' . $e->getMessage();
        }
        
        // Apply performance controls to limit issues if enabled
        if (class_exists('VMPFence_Performance_Manager')) {
            $issues = VMPFence_Performance_Manager::limit_issues($issues);
            $this->debug_log[] = 'Performance Manager: Limited issues to ' . count($issues);
        }
        
        return array(
            'issues' => $issues,
            'debug_log' => $this->debug_log,
            'scanned_count' => $scanned_count
        );
    }
    
    /**
     * Check if a scan should run based on configuration
     */
    private function should_run_scan($option_name) {
        if (!$this->scan_options || !isset($this->scan_options[$option_name])) {
            return true; // Default to enabled if not specified
        }
        return (bool) $this->scan_options[$option_name];
    }
    
    // NOTE: Admin user scanning has been moved to VMPFence_User_Security_Scanner
    // This class now focuses exclusively on WordPress options scanning
    
    private function scan_suspicious_options() {
        $this->debug_log[] = 'Scanning WordPress options for suspicious URLs and malicious content...';
        
        $issues = array();
        $options_checked = 0;
        
        global $wpdb;
        
        // Get comprehensive options scan
        $suspicious_options = $this->get_all_suspicious_options();
        
        $processed_options = 0;
        foreach ($suspicious_options as $option) {
            // Performance throttling for option processing
            if (class_exists('VMPFence_Performance_Manager')) {
                VMPFence_Performance_Manager::throttle_processing($processed_options);
            }
            $processed_options++;
            
            $option_name = $option['option_name'];
            $option_value = $option['option_value'];
            
            // Skip VMPFence options and other safe options
            if ($this->should_skip_option($option_name)) {
                $this->debug_log[] = "Skipping safe option: {$option_name}";
                continue;
            }
            
            $options_checked++;
            $this->debug_log[] = "Checking option: {$option_name}";
            
            // Skip empty or very short values
            if (empty($option_value) || strlen($option_value) < 10) {
                continue;
            }
            
            try {
                // Extract and analyze URLs from option value
                $urls = $this->extract_urls_from_content($option_value);
                
                foreach ($urls as $url) {
                    try {
                        // Enhanced suspicious URL analysis
                        $threat_level = $this->analyze_url_threat_level($url);
                        
                        if ($threat_level['is_suspicious']) {
                            $this->debug_log[] = "Suspicious URL found in option {$option_name}: {$url} (Score: {$threat_level['score']})";
                            
                            $issues[] = array(
                                'type' => 'suspicious_option_url',
                                'severity' => $this->map_threat_score_to_severity($threat_level['score']),
                                'title' => 'Suspicious URL in WordPress Option',
                                'description' => "Option '{$option_name}' contains a suspicious URL that matches {$threat_level['threat_type']} patterns",
                                'details' => "The URL '{$url}' in option '{$option_name}' has a threat score of {$threat_level['score']}/100. " . implode(', ', $threat_level['reasons']),
                                'recommendation' => 'Review this URL immediately. If unauthorized, remove it and scan for malware injection.',
                                'option_name' => $option_name,
                                'suspicious_url' => $url,
                                'threat_score' => $threat_level['score'],
                                'threat_reasons' => $threat_level['reasons']
                            );
                        }
                    } catch (Exception $e) {
                        $this->debug_log[] = "Error analyzing URL {$url} in option {$option_name}: " . $e->getMessage();
                        // Continue with next URL instead of failing entire scan
                        continue;
                    }
                }
                
                // Check for code injections in options (JavaScript, PHP, SQL)
                $injection_threats = $this->detect_code_injections($option_value, $option_name);
                $issues = array_merge($issues, $injection_threats);
                
            } catch (Exception $e) {
                $this->debug_log[] = "Error processing option {$option_name}: " . $e->getMessage();
                // Continue with next option instead of failing entire scan
                continue;
            }
        }
        
        $this->debug_log[] = "Checked {$options_checked} WordPress options for threats";
        
        return $issues;
    }
    
    /**
     * Get comprehensive list of WordPress options to scan
     */
    private function get_all_suspicious_options() {
        global $wpdb;
        
        $queries = array();
        
        // 1. All theme options (theme_mods, customizer settings)
        $queries[] = "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE 'theme_mods_%' OR option_name LIKE 'mods_%'";
        
        // 2. All widget options
        $queries[] = "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE 'widget_%'";
        
        // 3. Newspaper theme specific
        $queries[] = "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name REGEXP '^td_[0-9]+$' OR option_name = 'td_theme_options'";
        
        // 4. Transients (temporary data often used for malicious storage)
        $queries[] = "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE '%_transient_%' AND option_value LIKE '%http%'";
        
        // 5. Options containing URLs
        $queries[] = "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_value LIKE '%http%' AND LENGTH(option_value) > 20";
        
        // 6. Critical WordPress options
        $critical_options = array('home', 'siteurl', 'admin_email', 'new_admin_email', 'template', 'stylesheet', 'active_plugins');
        $queries[] = "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name IN ('" . implode("','", $critical_options) . "')";
        
        // 7. Plugin configuration options (common targets)
        $queries[] = "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE '%contact%' OR option_name LIKE '%form%' OR option_name LIKE '%seo%' OR option_name LIKE '%social%'";
        
        $all_options = array();
        $seen_options = array();
        
        // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Queries are static SQL with no user input
        foreach ($queries as $query) {
            $results = $wpdb->get_results($query, ARRAY_A);
            if ($results) {
                foreach ($results as $result) {
                    // Prevent duplicates
                    if (!isset($seen_options[$result['option_name']])) {
                        $all_options[] = $result;
                        $seen_options[$result['option_name']] = true;
                    }
                }
            }
        }
        // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
        
        // Handle multisite
        if (is_multisite()) {
            $blog_ids = get_sites(array('fields' => 'ids', 'number' => 100));
            foreach ($blog_ids as $blog_id) {
                if ($blog_id == get_current_blog_id()) continue;
                
                // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from WordPress core method
                $blog_table = $wpdb->get_blog_prefix($blog_id) . 'options';
                $blog_options = $wpdb->get_results(
                    "SELECT option_name, option_value FROM {$blog_table} WHERE option_value LIKE '%http%' LIMIT 50", 
                    ARRAY_A
                );
                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                
                if ($blog_options) {
                    foreach ($blog_options as $option) {
                        $option['option_name'] = "blog_{$blog_id}_{$option['option_name']}";
                        $all_options[] = $option;
                    }
                }
            }
        }
        
        return $all_options;
    }
    
    /**
     * Advanced URL threat analysis without external APIs
     */
    private function analyze_url_threat_level($url) {
        $parsed_url = wp_parse_url(strtolower($url));
        $threat_score = 0;
        $reasons = array();
        $threat_type = 'unknown';
        
        if (!$parsed_url || !isset($parsed_url['host'])) {
            return array('is_suspicious' => false, 'score' => 0, 'reasons' => array(), 'threat_type' => 'invalid');
        }
        
        $host = $parsed_url['host'];
        $path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
        $query = isset($parsed_url['query']) ? $parsed_url['query'] : '';
        
        // Skip our own domain and common legitimate domains
        $site_host = wp_parse_url(site_url(), PHP_URL_HOST);
        if ($host === strtolower($site_host)) {
            return array('is_suspicious' => false, 'score' => 0, 'reasons' => array(), 'threat_type' => 'local');
        }
        
        /**
         * Legitimate domain patterns for security scanning whitelist.
         * 
         * IMPORTANT: These are NOT external API calls or remote dependencies.
         * These domains are used as pattern matching strings to identify legitimate
         * URLs that may appear in WordPress options during security scanning.
         * 
         * Purpose: Prevent false positives when scanning for malicious URLs by
         * excluding well-known legitimate services (CDNs, social media, WordPress core).
         * 
         * No external requests are made to these domains by this plugin.
         * They are simply reference patterns for URL analysis.
         * 
         * @var array List of trusted domain patterns
         */
        $legitimate_domains = array(
            // WordPress core services
            'wordpress.org', 'wordpress.com', 'gravatar.com',
            
            // Google services (fonts, APIs, analytics)
            'google.com', 'googleapis.com', 'gstatic.com',
            
            // Social media embeds (oEmbed providers)
            'facebook.com', 'twitter.com', 'youtube.com', 'vimeo.com',
            'instagram.com', 'linkedin.com',
            
            // Development resources
            'github.com', 'jquery.com', 'jsdelivr.net',
            
            // CDN services (commonly used by themes/plugins)
            'cloudflare.com', 'cdnjs.cloudflare.com', 'bootstrapcdn.com', 'fontawesome.com'
        );
        
        foreach ($legitimate_domains as $domain) {
            if (strpos($host, $domain) !== false) {
                return array('is_suspicious' => false, 'score' => 0, 'reasons' => array(), 'threat_type' => 'legitimate');
            }
        }
        
        // HIGH RISK INDICATORS
        
        // Suspicious TLDs (high malware usage)
        $high_risk_tlds = array('.tk', '.ml', '.ga', '.cf', '.su', '.cc', '.ws', '.biz', '.info', '.top', '.click', '.download');
        foreach ($high_risk_tlds as $tld) {
            if (substr($host, -strlen($tld)) === $tld) {
                $threat_score += 40;
                $reasons[] = "Suspicious TLD: {$tld}";
                $threat_type = 'malware-tld';
            }
        }
        
        // IP addresses instead of domains
        if (preg_match('/^\d+\.\d+\.\d+\.\d+/', $host)) {
            $threat_score += 35;
            $reasons[] = 'IP address instead of domain name';
            $threat_type = 'ip-host';
        }
        
        // URL shorteners (often used to hide malicious destinations)
        $url_shorteners = array('bit.ly', 'tinyurl.com', 't.co', 'goo.gl', 'ow.ly', 'is.gd', 'buff.ly', 'rebrand.ly');
        foreach ($url_shorteners as $shortener) {
            if (strpos($host, $shortener) !== false) {
                $threat_score += 30;
                $reasons[] = "URL shortener: {$shortener}";
                $threat_type = 'url-shortener';
            }
        }
        
        // Known malware/phishing keywords in domain
        $malware_keywords = array('pharmacy', 'casino', 'poker', 'porn', 'adult', 'hack', 'crack', 'keygen', 'warez', 'torrent', 'pirate');
        foreach ($malware_keywords as $keyword) {
            if (strpos($host, $keyword) !== false) {
                $threat_score += 25;
                $reasons[] = "Malware keyword in domain: {$keyword}";
                $threat_type = 'malware-keyword';
            }
        }
        
        // Suspicious subdomains
        if (preg_match('/^(ads|ad|banner|click|download|free|get|install|play|stream|watch|win|bonus|prize)\./i', $host)) {
            $threat_score += 20;
            $reasons[] = 'Suspicious subdomain pattern';
            $threat_type = 'suspicious-subdomain';
        }
        
        // Very short domains (often used for malicious purposes)
        $domain_parts = explode('.', $host);
        if (isset($domain_parts[0]) && strlen($domain_parts[0]) <= 3 && !in_array($domain_parts[0], array('www', 'api', 'cdn', 'ssl'))) {
            $threat_score += 15;
            $reasons[] = 'Very short domain name';
            $threat_type = 'short-domain';
        }
        
        // Suspicious file extensions in path
        if (preg_match('/\.(exe|scr|bat|cmd|com|pif|vbs|js|jar|zip|rar)(\?|$)/i', $path)) {
            $threat_score += 45;
            $reasons[] = 'Executable file extension in URL path';
            $threat_type = 'executable-download';
        }
        
        // Non-standard ports
        if (isset($parsed_url['port']) && !in_array($parsed_url['port'], array(80, 443, 8080, 8443))) {
            $threat_score += 20;
            $reasons[] = "Non-standard port: {$parsed_url['port']}";
            $threat_type = 'non-standard-port';
        }
        
        // Suspicious path patterns
        $suspicious_paths = array(
            '/\/(ads|ad|click|download|install|malware|virus|trojan|backdoor)[\/?]/i' => 'Malicious path keywords',
            '/\/(wp-admin|admin|login).*\.(php|asp|jsp)$/i' => 'Admin panel access attempt',
            '/\.php\?.*eval.*=/i' => 'PHP code execution attempt',
            '/base64_decode/i' => 'Base64 decoding (obfuscation)'
        );
        
        foreach ($suspicious_paths as $pattern => $description) {
            if (preg_match($pattern, $path)) {
                $threat_score += 30;
                $reasons[] = $description;
                $threat_type = 'malicious-path';
            }
        }
        
        // Suspicious query parameters
        $suspicious_queries = array(
            '/eval\s*\(/i' => 'Code evaluation in query',
            '/base64_decode/i' => 'Base64 decoding in query',
            '/exec\s*\(/i' => 'Command execution in query',
            '/system\s*\(/i' => 'System command in query'
        );
        
        foreach ($suspicious_queries as $pattern => $description) {
            if (preg_match($pattern, $query)) {
                $threat_score += 35;
                $reasons[] = $description;
                $threat_type = 'malicious-query';
            }
        }
        
        // URL encoding obfuscation
        $encoded_count = substr_count($url, '%');
        if ($encoded_count > 10) {
            $threat_score += 15;
            $reasons[] = 'Excessive URL encoding (obfuscation)';
            $threat_type = 'obfuscated-url';
        }
        
        // Very long URLs (often used to hide malicious content)
        if (strlen($url) > 200) {
            $threat_score += 10;
            $reasons[] = 'Unusually long URL';
            $threat_type = 'long-url';
        }
        
        $is_suspicious = $threat_score >= 25; // Threshold for suspicious classification
        
        return array(
            'is_suspicious' => $is_suspicious,
            'score' => min($threat_score, 100), // Cap at 100
            'reasons' => $reasons,
            'threat_type' => $threat_type
        );
    }
    
    /**
     * Detect code injections in option values
     */
    private function detect_code_injections($option_value, $option_name) {
        $issues = array();
        $injection_score = 0;
        $detected_injections = array();
        
        try {
            // Convert non-string values safely
            if (!is_string($option_value)) {
                $option_value = $this->convert_to_searchable_string($option_value);
            }
            
            // Skip if empty after conversion
            if (empty($option_value)) {
                return array();
            }
        
        // JavaScript injection patterns
        $js_patterns = array(
            '/<script[^>]*>.*(?:eval|document\.write|window\.location|xhr|fetch)/is' => 'Malicious script tag with dangerous functions',
            '/javascript\s*:\s*(?:eval|alert|document\.write)/i' => 'JavaScript protocol with dangerous functions',
            '/eval\s*\(\s*(?:atob|unescape|String\.fromCharCode)/i' => 'JavaScript eval with obfuscation',
            '/document\.write\s*\(\s*["\']<script/i' => 'Dynamic script injection via document.write',
            '/window\.location\s*=\s*["\'](?:javascript|data):/i' => 'Malicious window location redirect',
            '/innerHTML\s*=\s*["\'].*<script/i' => 'Script injection via innerHTML',
            '/on(?:click|load|error|focus|blur)\s*=\s*["\'].*(?:eval|window\.location|document\.write)/i' => 'Event handler with malicious code'
        );
        
        // PHP injection patterns
        $php_patterns = array(
            '/<\?php\s+(?:eval|system|exec|shell_exec|passthru|file_get_contents|fopen|fwrite)/i' => 'PHP tag with dangerous function',
            '/eval\s*\(\s*(?:\$_(?:GET|POST|REQUEST|COOKIE)|base64_decode|gzinflate|str_rot13)/i' => 'PHP eval with user input or obfuscation',
            '/(?:system|exec|shell_exec|passthru)\s*\(\s*\$_(?:GET|POST|REQUEST)/i' => 'Command execution with user input',
            '/file_(?:get_contents|put_contents)\s*\(\s*(?:\$_(?:GET|POST)|["\'][^"\']*\.php)/i' => 'File manipulation with user input',
            '/(?:include|require)(?:_once)?\s*\(\s*\$_(?:GET|POST|REQUEST)/i' => 'File inclusion with user input',
            '/base64_decode\s*\(\s*["\'][A-Za-z0-9+\/]{20,}={0,2}["\'](?:\s*\)\s*;\s*eval)/i' => 'Base64 encoded malicious code',
            '/preg_replace\s*\(\s*["\'].*\/e["\'].*\$_(?:GET|POST)/i' => 'Code execution via preg_replace /e modifier'
        );
        
        // SQL injection patterns
        $sql_patterns = array(
            '/UNION\s+(?:ALL\s+)?SELECT.*(?:concat|group_concat|load_file|into\s+outfile)/i' => 'Advanced SQL injection with data extraction',
            '/(?:DROP|ALTER|CREATE)\s+(?:TABLE|DATABASE|USER)/i' => 'Destructive SQL commands',
            '/(?:DELETE|UPDATE)\s+.*WHERE.*(?:1\s*=\s*1|["\'].*["\'])/i' => 'SQL injection in DELETE/UPDATE',
            '/INSERT\s+INTO.*VALUES.*(?:char\(|0x[0-9a-f]+)/i' => 'SQL injection in INSERT with encoding',
            '/SELECT\s+.*FROM\s+.*WHERE.*(?:benchmark|sleep|waitfor)/i' => 'Time-based SQL injection'
        );
        
        // Check JavaScript injections
        foreach ($js_patterns as $pattern => $description) {
            if (preg_match($pattern, $option_value)) {
                $injection_score += 60;  // Increased score for more specific patterns
                $detected_injections[] = "JavaScript: {$description}";
            }
        }
        
        // Check PHP injections (highest scores for most dangerous patterns)
        foreach ($php_patterns as $pattern => $description) {
            if (preg_match($pattern, $option_value)) {
                $injection_score += 70;  // Increased score for specific PHP threats
                $detected_injections[] = "PHP: {$description}";
            }
        }
        
        // Check SQL injections (high scores for database threats)
        foreach ($sql_patterns as $pattern => $description) {
            if (preg_match($pattern, $option_value)) {
                $injection_score += 65;  // High score for SQL threats
                $detected_injections[] = "SQL: {$description}";
            }
        }
        
        // Additional context checks to reduce false positives
        $false_positive_indicators = array(
            '/(?:wp-admin|wp-content|wp-includes)\//',  // WordPress core paths
            '/(?:theme|plugin)_(?:options|settings)/',  // Plugin/theme settings
            '/(?:customizer|widgets?)_/',               // WordPress customizer/widgets
            '/^a:\d+:\{.*\}$/',                        // Serialized WordPress data
        );
        
        $is_likely_legitimate = false;
        foreach ($false_positive_indicators as $indicator) {
            if (preg_match($indicator, $option_value)) {
                $is_likely_legitimate = true;
                break;
            }
        }
        
        // Adjust threshold based on context
        $threshold = $is_likely_legitimate ? 80 : 60;  // Higher threshold for legitimate-looking content
        
        // If significant injection detected, create issue
        if ($injection_score >= $threshold) {
            $severity = $injection_score >= 80 ? 'critical' : 'medium';
            
            $issues[] = array(
                'type' => 'code_injection_in_option',
                'severity' => $severity,
                'title' => 'Code Injection in WordPress Option',
                'description' => "Option '{$option_name}' contains suspicious code that appears to be malicious injection",
                'details' => "Detected injections: " . implode(', ', $detected_injections) . ". Injection score: {$injection_score}/100" . ($is_likely_legitimate ? " (Adjusted threshold due to legitimate context)" : ""),
                'recommendation' => $injection_score >= 80 ? 'URGENT: This option contains malicious code. Clean or restore from backup immediately.' : 'Review this option for potential malicious code injection.',
                'option_name' => $option_name,
                'injection_types' => $detected_injections,
                'injection_score' => $injection_score
            );
        }
        
        } catch (Exception $e) {
            // Log error but don't fail entire scan
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Intentional for debugging
            error_log("VMPFence: Error detecting code injections in option {$option_name}: " . $e->getMessage());
        }
        
        return $issues;
    }
    
    /**
     * Map threat score to severity level
     */
    private function map_threat_score_to_severity($score) {
        if ($score >= 75) return 'critical';
        if ($score >= 50) return 'high';
        if ($score >= 25) return 'medium';
        return 'low';
    }
    
    // NOTE: Admin user methods moved to VMPFence_User_Security_Scanner
    
    private function get_scanned_options_count() {
        global $wpdb;
        $count = $wpdb->get_var(
            "SELECT COUNT(*) FROM {$wpdb->options} 
             WHERE option_name LIKE 'widget_%' 
             OR option_name LIKE 'theme_mods_%'
             OR option_value LIKE '%http%'"
        );
        return (int) $count;
    }
    
    private function extract_urls_from_content($content) {
        $urls = array();
        
        if (empty($content)) {
            return $urls;
        }
        
        // Handle serialized data (common in WordPress options)
        if (is_serialized($content)) {
            $unserialized = @unserialize($content);
            if ($unserialized !== false) {
                // Use secure iterative conversion
                $content = $this->convert_to_searchable_string($unserialized);
            }
        }
        
        // Ensure content is a string
        if (!is_string($content)) {
            $content = $this->convert_to_searchable_string($content);
        }
        
        // For very large content, process in chunks to avoid memory issues
        $content_length = strlen($content);
        if ($content_length > 100000) { // 100KB threshold
            return $this->extract_urls_from_large_content($content);
        }
        
        // Multiple URL patterns to catch different formats
        $url_patterns = array(
            // Standard HTTP/HTTPS URLs
            '/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,8}\b([-a-zA-Z0-9()@:%_\+.~#?&\/=]*)/i',
            
            // URLs without http/https prefix
            '/(?:^|[\s"\'\(\)<>])([a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,8}(?:\/[^\s"\'\(\)<>]*)?/i',
            
            // FTP URLs
            '/ftp:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/=]*)/i',
            
            // IPv4 addresses with protocols
            '/https?:\/\/(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?(?:\/[^\s]*)?/i',
            
            // IPv6 addresses with protocols (RFC 3986 bracket notation)
            '/https?:\/\/\[(?:[0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}\](?::\d+)?(?:\/[^\s]*)?/i',
            
            // Base64 encoded URLs (common obfuscation technique)
            '/aHR0cHM{0,1}[A-Za-z0-9+\/=]+/',
            
            // JavaScript URL constructors
            '/(?:window\.location|document\.location|location\.href)\s*=\s*["\']([^"\']+)["\']/i',
            
            // HTML attributes containing URLs
            '/(?:src|href|action|data-url)\s*=\s*["\']([^"\']+)["\']/i'
        );
        
        foreach ($url_patterns as $pattern) {
            preg_match_all($pattern, $content, $matches);
            
            if (!empty($matches[1])) {
                // Use captured group for attribute patterns
                $urls = array_merge($urls, $matches[1]);
            } elseif (!empty($matches[0])) {
                // Use full match for direct URL patterns
                $urls = array_merge($urls, $matches[0]);
            }
        }
        
        // Clean and validate URLs
        $cleaned_urls = array();
        foreach ($urls as $url) {
            $url = trim($url, '"\'()[]<> ');
            
            // Skip very short URLs or non-URLs
            if (strlen($url) < 10) {
                continue;
            }
            
            // Add protocol if missing for domain-only matches
            if (!preg_match('/^https?:\/\//i', $url) && preg_match('/^[a-zA-Z0-9]/', $url)) {
                $url = 'http://' . $url;
            }
            
            // Decode Base64 URLs
            if (preg_match('/^aHR0cHM{0,1}/', $url)) {
                $decoded = @base64_decode($url);
                if ($decoded && filter_var($decoded, FILTER_VALIDATE_URL)) {
                    $url = $decoded;
                }
            }
            
            // Final validation (supports both IPv4 and IPv6)
            if (filter_var($url, FILTER_VALIDATE_URL) || 
                preg_match('/^https?:\/\/(?:\d{1,3}\.){3}\d{1,3}/i', $url) ||
                preg_match('/^https?:\/\/\[(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}\]/i', $url)) {
                $cleaned_urls[] = $url;
            }
        }
        
        return array_unique($cleaned_urls);
    }
    
    /**
     * Convert any data type to a searchable string for URL extraction
     * Uses iterative stack-based approach to prevent memory exhaustion
     * Handles circular references and unlimited depth safely
     */
    private function convert_to_searchable_string($data, $max_depth = 50) {
        $result_parts = array();
        $stack = array(array('data' => $data, 'depth' => 0));
        $visited_objects = array(); // Track objects for circular reference detection (persistent across all processing)
        $chunk_size = 1000; // Process results in chunks to avoid memory spikes
        
        while (!empty($stack)) {
            $item = array_pop($stack);
            $current_data = $item['data'];
            $depth = $item['depth'];
            
            // Handle strings directly
            if (is_string($current_data)) {
                $result_parts[] = $current_data;
                
                // Process in chunks to free memory
                if (count($result_parts) >= $chunk_size) {
                    $temp = implode(' ', $result_parts);
                    $result_parts = array($temp);
                }
                continue;
            }
            
            // Handle arrays
            if (is_array($current_data)) {
                // Skip empty arrays
                if (empty($current_data)) {
                    continue;
                }
                
                // At max depth, use fallback but still process everything
                if ($depth >= $max_depth) {
                    // Fallback: convert to JSON (still processes all data)
                    $json = @json_encode($current_data);
                    if ($json !== false) {
                        $result_parts[] = $json;
                    }
                } else {
                    // Push array items to stack (reverse order for correct processing)
                    foreach (array_reverse($current_data, true) as $value) {
                        $stack[] = array('data' => $value, 'depth' => $depth + 1);
                    }
                }
                continue;
            }
            
            // Handle objects with circular reference detection
            if (is_object($current_data)) {
                $obj_id = spl_object_id($current_data);
                
                // Check for circular reference - MUST stay in visited for entire processing
                if (isset($visited_objects[$obj_id])) {
                    $result_parts[] = '[Circular]';
                    continue;
                }
                
                // Mark as visited PERMANENTLY (not removed until end)
                $visited_objects[$obj_id] = true;
                
                // At max depth, use fallback
                if ($depth >= $max_depth) {
                    $json = @json_encode($current_data);
                    if ($json !== false) {
                        $result_parts[] = $json;
                    }
                } else {
                    // Push object properties to stack
                    $properties = get_object_vars($current_data);
                    if (!empty($properties)) {
                        foreach (array_reverse($properties, true) as $value) {
                            $stack[] = array('data' => $value, 'depth' => $depth + 1);
                        }
                    }
                }
                // DO NOT unset visited_objects here - must persist!
                continue;
            }
            
            // Handle other types
            if (is_bool($current_data)) {
                $result_parts[] = $current_data ? '1' : '0';
            } elseif (is_numeric($current_data)) {
                $result_parts[] = (string) $current_data;
            } elseif (is_resource($current_data)) {
                // Resources cannot be converted to strings safely - skip them
                $result_parts[] = '[Resource]';
            } elseif (is_null($current_data)) {
                // Null values - explicitly handle
                $result_parts[] = '';
            } else {
                // Try to convert remaining types to string
                $result_parts[] = @(string) $current_data;
            }
        }
        
        // Clear visited objects tracking (all processing complete)
        unset($visited_objects);
        
        // Handle empty result
        if (empty($result_parts)) {
            return '';
        }
        
        // Final assembly in chunks to avoid memory spike
        $chunk_count = ceil(count($result_parts) / $chunk_size);
        $final_result = '';
        
        for ($i = 0; $i < $chunk_count; $i++) {
            $chunk = array_slice($result_parts, $i * $chunk_size, $chunk_size);
            $final_result .= implode(' ', $chunk) . ' ';
            unset($chunk); // Free memory immediately
        }
        
        return trim($final_result);
    }
    
    /**
     * Extract URLs from large content using chunked processing
     * Prevents memory exhaustion on multi-MB strings
     */
    private function extract_urls_from_large_content($content) {
        $urls = array();
        $chunk_size = 100000; // 100KB chunks
        $overlap = 500; // Overlap to catch URLs spanning chunk boundaries
        $content_length = strlen($content);
        
        for ($offset = 0; $offset < $content_length; $offset += $chunk_size) {
            // Get chunk with overlap
            $chunk_start = max(0, $offset - $overlap);
            $chunk = substr($content, $chunk_start, $chunk_size + ($overlap * 2));
            
            // Extract URLs from this chunk
            $chunk_urls = $this->extract_urls_from_string($chunk);
            $urls = array_merge($urls, $chunk_urls);
            
            // Free chunk memory
            unset($chunk, $chunk_urls);
            
            // Periodic garbage collection
            if ($offset % 500000 === 0) {
                gc_collect_cycles();
            }
        }
        
        // Remove duplicates
        return array_unique($urls);
    }
    
    /**
     * Extract URLs from a string using regex patterns
     * Includes cleaning and validation
     */
    private function extract_urls_from_string($string) {
        $urls = array();
        
        // URL patterns (same as original)
        $url_patterns = array(
            '/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,8}\b([-a-zA-Z0-9()@:%_\+.~#?&\/=]*)/i',
            '/(?:^|[\s"\'\(\)<>])([a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,8}(?:\/[^\s"\'\(\)<>]*)?/i',
            '/ftp:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/=]*)/i',
            '/https?:\/\/(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?(?:\/[^\s]*)?/i',
            '/aHR0cHM{0,1}[A-Za-z0-9+\/=]+/', // Base64 encoded URLs
        );
        
        foreach ($url_patterns as $pattern) {
            if (preg_match_all($pattern, $string, $matches)) {
                if (!empty($matches[1])) {
                    $urls = array_merge($urls, $matches[1]);
                } elseif (!empty($matches[0])) {
                    $urls = array_merge($urls, $matches[0]);
                }
            }
        }
        
        // Clean and validate URLs
        $cleaned_urls = array();
        foreach ($urls as $url) {
            $url = trim($url, '"\'\'()[]<> ');
            
            // Skip very short URLs
            if (strlen($url) < 10) {
                continue;
            }
            
            // Add protocol if missing
            if (!preg_match('/^https?:\/\//i', $url) && preg_match('/^[a-zA-Z0-9]/', $url)) {
                $url = 'http://' . $url;
            }
            
            // Decode Base64 URLs
            if (preg_match('/^aHR0cHM{0,1}/', $url)) {
                $decoded = @base64_decode($url);
                if ($decoded && filter_var($decoded, FILTER_VALIDATE_URL)) {
                    $url = $decoded;
                }
            }
            
            // Final validation (supports both IPv4 and IPv6)
            if (filter_var($url, FILTER_VALIDATE_URL) || 
                preg_match('/^https?:\/\/(?:\d{1,3}\.){3}\d{1,3}/i', $url) ||
                preg_match('/^https?:\/\/\[(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}\]/i', $url)) {
                $cleaned_urls[] = $url;
            }
        }
        
        return $cleaned_urls;
    }
    
    /**
     * Get current memory usage percentage
     */
    private function get_memory_usage_percentage() {
        $limit = $this->get_memory_limit_bytes();
        if ($limit === -1) {
            return 0;
        }
        $usage = memory_get_usage(true);
        return ($usage / $limit) * 100;
    }
    
    /**
     * Get PHP memory limit in bytes
     */
    private function get_memory_limit_bytes() {
        $limit = ini_get('memory_limit');
        
        if ($limit == -1) {
            return -1;
        }
        
        $value = (int) $limit;
        $unit = strtoupper(substr($limit, -1));
        
        switch ($unit) {
            case 'G':
                return $value * 1024 * 1024 * 1024;
            case 'M':
                return $value * 1024 * 1024;
            case 'K':
                return $value * 1024;
            default:
                return $value;
        }
    }
    
    private function is_suspicious_url($url) {
        $parsed_url = wp_parse_url(strtolower($url));
        
        if (!$parsed_url || !isset($parsed_url['host'])) {
            return false;
        }
        
        $host = $parsed_url['host'];
        $path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
        
        // Skip our own domain
        $site_host = wp_parse_url(site_url(), PHP_URL_HOST);
        if ($host === strtolower($site_host)) {
            return false;
        }
        
        /**
         * Legitimate domain patterns for security scanning whitelist.
         * 
         * IMPORTANT: These are NOT external API calls or remote dependencies.
         * These domains are used as pattern matching strings to identify legitimate
         * URLs during security scanning of WordPress options.
         * 
         * Purpose: Reduce false positives by excluding trusted domains from
         * suspicious URL detection. No external requests are made to these domains.
         * 
         * @var array List of trusted domain patterns for URL analysis
         */
        $legitimate_domains = array(
            // WordPress core services
            'wordpress.org',
            'wordpress.com',
            'gravatar.com',
            
            // Google services (fonts, APIs, analytics)
            'google.com',
            'googleapis.com',
            'gstatic.com',
            
            // Social media embeds (oEmbed providers)
            'facebook.com',
            'twitter.com',
            'youtube.com',
            'vimeo.com',
            'instagram.com',
            'linkedin.com',
            
            // Development resources
            'github.com',
            'jquery.com',
            'jsdelivr.net',
            
            // CDN services (commonly used by themes/plugins)
            'cloudflare.com',
            'cdnjs.cloudflare.com'
        );
        
        foreach ($legitimate_domains as $domain) {
            if (strpos($host, $domain) !== false) {
                return false;
            }
        }
        
        // Check for suspicious patterns
        $suspicious_patterns = array(
            // Suspicious TLDs
            '/\.(tk|ml|ga|cf|bit|ly|su|ru|cn)$/i',
            // IP addresses
            '/^\d+\.\d+\.\d+\.\d+/',
            // Very short domains
            '/^[a-z]{1,3}\.[a-z]{2,3}$/i',
            // Suspicious subdomains
            '/^(ads|ad|banner|click|download|free|get|install|play|stream|watch|win)\./i',
        );
        
        foreach ($suspicious_patterns as $pattern) {
            if (preg_match($pattern, $host)) {
                return true;
            }
        }
        
        // Check for suspicious path patterns
        $suspicious_path_patterns = array(
            '/\/(ads|ad|click|download|install|malware|virus|trojan|backdoor)[\/?]/i',
            '/\.(exe|scr|bat|cmd|com|pif|vbs|js)$/i',
            '/\/(wp-admin|admin|login).*\.(php|asp|jsp)$/i',
        );
        
        foreach ($suspicious_path_patterns as $pattern) {
            if (preg_match($pattern, $path)) {
                return true;
            }
        }
        
        return false;
    }
    
    // Note: is_suspicious_url method has been replaced by analyze_url_threat_level 
    // for more comprehensive threat analysis
    
    public function get_debug_log() {
        return $this->debug_log;
    }
    
    /**
     * Clean up old user audit issues before running new scan
     */
    private function cleanup_old_issues() {
        global $wpdb;
        
        // Clear previous scan issues from new table structure
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from WordPress prefix
        $issues_table = $wpdb->prefix . 'vmpfence_scan_issues';
        
        $deleted = $wpdb->query($wpdb->prepare(
            "DELETE FROM {$issues_table} WHERE issue_type = %s AND status != 'ignored'", 'user_audit'
        ));
        
        // Clear previous scan results
        $results_table = $wpdb->prefix . 'vmpfence_scan_results';
        $deleted_results = $wpdb->query($wpdb->prepare(
            "DELETE FROM {$results_table} WHERE scan_type = %s", 'user_audit'
        ));
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        
        
        if ($deleted !== false) {
            $this->debug_log[] = "Cleaned up {$deleted} old user audit issues and {$deleted_results} scan results from database";
        } else {
            $this->debug_log[] = "Failed to clean up old user audit issues";
        }
    }
    
    /**
     * Store issues in the database
     *
     * @param array $results Array of issue results
     * @param int $scanned_count Number of items scanned
     */
    private function store_issues($results, $scanned_count = 0) {
        global $wpdb;
        
        $issues_count = is_array($results) ? count($results) : 0;
        
        // Save scan summary to scan_results table (ALWAYS save, even if no issues found)
        $scan_results_table = $wpdb->prefix . 'vmpfence_scan_results';
        $scan_result_id = $wpdb->insert(
            $scan_results_table,
            [
                'scan_type' => 'user_audit',
                'scan_time' => current_time('mysql'),
                'total_files' => $scanned_count, // Items checked during scan
                'issues_found' => $issues_count,
                'scan_status' => 'completed',
                'scan_data' => wp_json_encode([
                    'checks_performed' => $scanned_count,
                    'scan_type' => 'user_audit'
                ])
            ]
        );
        
        if ($scan_result_id === false) {
            $this->debug_log[] = 'Error saving scan summary: ' . $wpdb->last_error;
            return;
        }
        
        $scan_id = $wpdb->insert_id;
        $this->debug_log[] = "Saved user audit scan summary (ID: {$scan_id}) with {$issues_count} issues found from {$scanned_count} items checked";
        
        // If no issues found, we're done (but we did save the scan record)
        if (empty($results)) {
            $this->debug_log[] = "No user audit issues found, but scan record saved successfully";
            return;
        }
        
        // Save individual issues to scan_issues table
        $scan_issues_table = $wpdb->prefix . 'vmpfence_scan_issues';
        
        foreach ($results as $result) {
            $file_path = '';
            if (isset($result['username'])) {
                $file_path = 'user:' . $result['username'];
            } elseif (isset($result['option_name'])) {
                $file_path = 'option:' . $result['option_name'];
            }
            
            $description = $result['title'] . ': ' . $result['description'];
            $specific_type = $result['type'] ?? 'user_audit_unknown'; // Get specific type from result
            
            // First check if this issue should be recreated as ignored
            if ($this->handle_ignored_issue($file_path, 'user_audit', $description, $scan_id, $specific_type)) {
                continue; // Issue was handled as ignored, skip normal processing
            }
            
            // Check if this issue is currently ignored (skip normal creation)
            if ($this->is_issue_ignored($file_path, 'user_audit', $description)) {
                $this->debug_log[] = 'SKIP: User audit issue already ignored - ' . $result['title'];
                continue;
            }

            $insert_result = $wpdb->insert(
                $scan_issues_table,
                [
                    'scan_id' => $scan_id,
                    'file_path' => $file_path,
                    'issue_type' => 'user_audit',
                    'type' => $specific_type,
                    'severity' => $this->map_issue_type_to_severity('user_audit'),
                    'description' => $description,
                    'detected_time' => current_time('mysql'),
                    'status' => 'new'
                ]
            );
            
            if ($insert_result === false) {
                $this->debug_log[] = 'Error saving user audit issue: ' . $wpdb->last_error;
            }
        }
        
        $this->debug_log[] = "Saved scan summary (ID: " . $scan_id . ") and " . count($results) . " user audit issues to database";
    }
    
    /**
     * Check if an issue is already ignored
     */
    private function is_issue_ignored($file_path, $issue_type, $description) {
        global $wpdb;
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from WordPress prefix
        $scan_issues_table = $wpdb->prefix . 'vmpfence_scan_issues';
        
        $count = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM $scan_issues_table 
             WHERE file_path = %s AND issue_type = %s AND description = %s AND status = 'ignored'",
            $file_path,
            $issue_type,
            $description
        ));
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        
        return $count > 0;
    }
    
    /**
     * Handle previously ignored issues - recreate them in current scan as ignored
     *
     * @param string $file_path File path
     * @param string $issue_type Issue type
     * @param string $description Issue description
     * @param int $scan_id Current scan ID
     * @param string $specific_type Specific type for this issue
     * @return bool Whether issue was handled
     */
    private function handle_ignored_issue($file_path, $issue_type, $description, $scan_id, $specific_type = '') {
        global $wpdb;
        
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from WordPress prefix
        $issues_table = $wpdb->prefix . 'vmpfence_scan_issues';
        
        // Check if this exact issue was previously ignored
        $ignored_issue = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$issues_table} WHERE file_path = %s AND issue_type = %s AND description = %s AND status = 'ignored' ORDER BY detected_time DESC LIMIT 1",
            $file_path,
            $issue_type,
            $description
        ));
        
        if ($ignored_issue) {
            // Recreate the ignored issue in current scan
            $insert_result = $wpdb->insert(
                $issues_table,
                [
                    'scan_id' => $scan_id,
                    'file_path' => $file_path,
                    'issue_type' => $issue_type,
                    'type' => $specific_type ?: ($ignored_issue->type ?: 'user_audit_unknown'),
                    'severity' => $this->map_issue_type_to_severity($issue_type),
                    'description' => $description,
                    'status' => 'ignored',
                    'detected_time' => current_time('mysql'),
                    'resolved_time' => current_time('mysql')
                ],
                ['%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']
            );
            
            if ($insert_result) {
                // Clean up old ignored entries for this same issue (keep only the latest)
                $wpdb->query($wpdb->prepare(
                    "DELETE FROM {$issues_table} WHERE file_path = %s AND issue_type = %s AND description = %s AND status = 'ignored' AND id != %d",
                    $file_path,
                    $issue_type,
                    $description,
                    $wpdb->insert_id
                ));
                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                
                $this->debug_log[] = '[' . current_time('H:i:s') . '] PRESERVED: Recreated ignored user_audit - ' . $description;
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Map issue type to severity level
     *
     * @param string $issue_type Issue type
     * @return string Severity level
     */
    private function map_issue_type_to_severity($issue_type) {
        switch ($issue_type) {
            case 'user_audit':
                return 'medium';
            default:
                return 'medium';
        }
    }

    /**
     * Map text severity to numeric value for database storage
     *
     * @param string $severity Severity text (critical, high, medium, low)
     * @return int Numeric severity value
     */
    private function map_severity_to_number($severity) {
        switch (strtolower($severity)) {
            case 'critical':
                return 10;
            case 'high':
                return 7;
            case 'medium':
                return 5;
            case 'low':
                return 3;
            default:
                return 5; // Default to medium
        }
    }
}
