<?php
/**
 * VMPFence Early Firewall Protection
 * 
 * This file loads BEFORE regular plugins to catch direct file access attempts
 * and other attacks that try to bypass WordPress plugin loading.
 * 
 * Place this in: wp-content/mu-plugins/vmpfence-early-protection.php
 * 
 * @package VMPFence
 * @since 2.1.0
 * @since 2.2.2 - Added .env, phpinfo.php, .git directory blocks before WP loads
 * @since 2.2.4 - Added VMPFENCE_WAF_LOADED constant check
 */

// If VMP WAF is already loaded via auto_prepend_file, skip this mu-plugin
// This prevents redundant execution when WAF optimization is active
if (defined('VMPFENCE_WAF_LOADED')) {
    // error_log('VMPFence MU-Plugin: Skipping execution - WAF already loaded via auto_prepend_file');
    return;
}

// This must be in mu-plugins to load before other plugins
if (!defined('ABSPATH')) {
    // WordPress not loaded yet - apply basic protection
    
    $request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
    $script_filename = isset($_SERVER['SCRIPT_FILENAME']) ? $_SERVER['SCRIPT_FILENAME'] : '';
    
    // CRITICAL: Block configuration/info disclosure files BEFORE WordPress loads
    // Parse URL path (strip query strings)
    $path = parse_url($request_uri, PHP_URL_PATH);
    if ($path === false || $path === null) {
        // Malformed URL - block for safety
        http_response_code(403);
        die('Malformed request');
    }
    
    // Block .env files (anywhere in path, not just root)
    if (preg_match('/\/\.env(?:\.local|\.production|\.backup|\.example)?$/i', $path) ||
        preg_match('/^\.env(?:\.local|\.production|\.backup|\.example)?$/i', $path)) {
        http_response_code(403);
        header('Content-Type: text/plain; charset=utf-8');
        die('403 Forbidden: Configuration file access denied');
    }
    
    // Block phpinfo.php and test.php (root or any directory)
    if (preg_match('/\/(?:phpinfo|test|info)\.php$/i', $path) ||
        preg_match('/^(?:phpinfo|test|info)\.php$/i', $path)) {
        http_response_code(403);
        header('Content-Type: text/plain; charset=utf-8');
        die('403 Forbidden: Info disclosure file access denied');
    }
    
    // Block .git directory access (anywhere in path)
    if (preg_match('/\/\.git\//i', $path) || preg_match('/\/\.git$/i', $path)) {
        http_response_code(403);
        header('Content-Type: text/plain; charset=utf-8');
        die('403 Forbidden: Version control directory access denied');
    }
    
    // Block direct access to PHP files in wp-content (except mu-plugins)
    if (strpos($script_filename, 'wp-content') !== false) {
        // Allow mu-plugins (needed for WordPress to function)
        if (strpos($script_filename, 'mu-plugins') !== false) {
            return;
        }
        
        // Block access to plugins, themes, uploads PHP files
        if (strpos($script_filename, '/plugins/') !== false ||
            strpos($script_filename, '/themes/') !== false ||
            strpos($script_filename, '/uploads/') !== false) {
            
            // Log the attempt
            // error_log('VMPFence EARLY BLOCK: Direct PHP access - ' . $script_filename);
            
            http_response_code(403);
            header('Content-Type: text/html; charset=utf-8');
            echo '<!DOCTYPE html>
<html>
<head>
    <title>403 Forbidden</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 50px; background: #f5f5f5; }
        .container { max-width: 600px; margin: 0 auto; background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { color: #d32f2f; margin-top: 0; }
        .warning { background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0; }
        code { background: #f5f5f5; padding: 2px 6px; border-radius: 3px; }
    </style>
</head>
<body>
    <div class="container">
        <h1>⛔ Access Denied</h1>
        <div class="warning">
            <strong>Direct PHP file access is not permitted.</strong>
            <p>For security reasons, PHP files in WordPress plugins, themes, and uploads directories cannot be accessed directly.</p>
        </div>
        <p><strong>Blocked by:</strong> VMPFence Early Protection</p>
        <p><strong>Time:</strong> ' . date('Y-m-d H:i:s') . '</p>
        <p><strong>IP:</strong> ' . (isset($_SERVER['REMOTE_ADDR']) ? htmlspecialchars($_SERVER['REMOTE_ADDR']) : 'Unknown') . '</p>
        <hr>
        <p style="font-size: 12px; color: #666;">If you believe this is an error, contact the site administrator.</p>
    </div>
</body>
</html>';
            exit;
        }
    }
    
    return; // Not in ABSPATH context, exit early
}

// WordPress is loaded - now apply advanced protection

/**
 * Early firewall initialization
 * Runs before regular plugins to catch attacks early
 */
class VMPFence_Early_Protection {
    
    private static $instance = null;
    
    public static function get_instance() {
        if (null === self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    private function __construct() {
        // Hook very early
        add_action('muplugins_loaded', array($this, 'init'), 1);
    }
    
    public function init() {
        // Block suspicious requests before WordPress fully loads
        $this->block_suspicious_requests();
        $this->block_automated_scanners();
        $this->enforce_rate_limiting();
    }
    
    /**
     * Block suspicious request patterns
     * @since 2.2.2 - Enhanced pattern checks and error handling
     */
    private function block_suspicious_requests() {
        $request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
        
        // Parse URL path (strip query strings) with error handling
        $path = parse_url($request_uri, PHP_URL_PATH);
        if ($path === false || $path === null) {
            // Malformed URL - block for safety
            $this->block_request('Malformed request URL');
        }
        
        // NOTE: Configuration file blocks (.env, phpinfo.php, .git) are handled in the
        // pre-WordPress section above (lines 30-52) for maximum protection.
        // WAF Controller handles all other patterns via database rules.
        
        // Dangerous patterns that should never appear in legitimate requests
        $dangerous_patterns = array(
            '/etc/passwd',
            '/etc/shadow',
            'wp-config.php',
            'php://input',
            'php://filter',
            'data:text/html',
            'eval(',
            'base64_decode',
            'system(',
            'exec(',
            'passthru(',
            'shell_exec',
            'proc_open',
            'popen(',
        );
        
        // Check path for dangerous patterns
        foreach ($dangerous_patterns as $pattern) {
            if (stripos($path, $pattern) !== false) {
                $this->block_request('Dangerous pattern detected: ' . $pattern);
            }
        }
        
        // Check POST data for dangerous patterns (if not admin)
        // SECURITY FIX: Add defensive isset() check for REQUEST_METHOD to handle CLI/edge cases
        if (!is_admin() && isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST)) {
            $post_data = json_encode($_POST);
            foreach ($dangerous_patterns as $pattern) {
                if (stripos($post_data, $pattern) !== false) {
                    $this->block_request('Dangerous POST pattern detected');
                }
            }
        }
    }
    
    /**
     * Block automated scanner user agents
     */
    private function block_automated_scanners() {
        $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower($_SERVER['HTTP_USER_AGENT']) : '';
        
        // Known malicious scanners
        $scanner_signatures = array(
            'sqlmap',
            'nikto',
            'acunetix',
            'nessus',
            'masscan',
            'w3af',
            'nmap',
            'zgrab',
            'whatweb',
            'dirbuster',
            'wpscan',
            'metasploit',
            'burpsuite',
            'havij',
            'python-requests', // Often used in malicious scripts
            'scrapy',
        );
        
        foreach ($scanner_signatures as $signature) {
            if (strpos($user_agent, $signature) !== false) {
                $this->block_request('Automated scanner detected: ' . $signature, 429);
            }
        }
        
        // Block requests with no user agent (common in automated attacks)
        if (empty($user_agent) && !$this->is_allowed_empty_ua()) {
            $this->block_request('Empty User-Agent blocked', 403);
        }
    }
    
    /**
     * Basic rate limiting for automated scripts
     * @since 2.2.2 -- Conditional skips for admin pages
     */
    private function enforce_rate_limiting() {
        // Skip rate limiting for wp-cron.php (WordPress internal requests)
        $request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
        if (strpos($request_uri, 'wp-cron.php') !== false) {
            return;
        }
        
        // Conditional skip for admin pages - only legitimate browsers
        if (strpos($request_uri, 'wp-admin') !== false || 
            strpos($request_uri, 'wp-login.php') !== false) {
            
            // Skip for WP-CLI (legitimate command-line operations)
            if (defined('WP_CLI') && WP_CLI) {
                return;
            }
            
            // Check if legitimate browser
            $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
            if ($this->is_legitimate_browser($user_agent)) {
                return; // Browser allowed, skip rate limit
            }
            // Bot/curl continues to rate limiting below
        }
        
        $ip = $this->get_client_ip();
        
        // Use transients for simple rate limiting (no database)
        $transient_key = 'vmpfence_rl_' . md5($ip);
        $requests = get_transient($transient_key);
        
        if ($requests === false) {
            // First request in this window
            set_transient($transient_key, 1, 60); // 60 second window
        } else {
            $requests++;
            
            // Increased to 240 requests per minute (reasonable for normal browsing with AJAX)
            if ($requests > 240) {
                $this->block_request('Rate limit exceeded', 429);
            }
            
            set_transient($transient_key, $requests, 60);
        }
    }
    
    /**
     * Check if empty UA is allowed (e.g., cron, localhost)
     */
    private function is_allowed_empty_ua() {
        // Allow wp-cron
        if (defined('DOING_CRON') && DOING_CRON) {
            return true;
        }
        
        // Allow wp-cron.php from ANY IP (WordPress internal requests)
        // WP-Cron spawns internal HTTP requests without user-agent
        $request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
        if (strpos($request_uri, 'wp-cron.php') !== false) {
            return true;
        }
        
        // Allow xmlrpc and specific WordPress files from localhost only
        $ip = $this->get_client_ip();
        if (in_array($ip, array('127.0.0.1', '::1'))) {
            if (strpos($request_uri, 'xmlrpc.php') !== false) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Check if User-Agent represents a legitimate browser
     * Early protection cannot use VMPFence_Visitor_Classifier (not loaded yet)
     * Inline implementation that mirrors Visitor Classifier logic
     * 
     * @since 2.2.2
     * @param string $user_agent User-Agent string
     * @return bool True if legitimate browser, false if bot/curl/automation
     */
    private function is_legitimate_browser($user_agent) {
        // Empty user agent = bot
        if (empty($user_agent)) {
            return false;
        }
        
        $user_agent_lower = strtolower($user_agent);
        
        // Known browser patterns (must have at least one)
        $browser_patterns = array('mozilla', 'chrome', 'safari', 'firefox', 'edge', 'opera', 'msie', 'trident');
        $has_browser_pattern = false;
        foreach ($browser_patterns as $pattern) {
            if (strpos($user_agent_lower, $pattern) !== false) {
                $has_browser_pattern = true;
                break;
            }
        }
        
        // If no browser pattern, it's definitely a bot
        if (!$has_browser_pattern) {
            return false;
        }
        
        // Has browser pattern, but check for explicit bot/crawler keywords
        // These indicate crawler even if browser-like UA (matches Visitor Classifier logic)
        $bot_patterns = array('bot', 'spider', 'crawl', 'slurp', 'curl', 'wget', 'python-requests', 'scrapy', 'headless', 'phantom', 'selenium');
        foreach ($bot_patterns as $pattern) {
            if (strpos($user_agent_lower, $pattern) !== false) {
                return false; // Bot disguised as browser
            }
        }
        
        // Has browser pattern, no bot keywords = legitimate browser
        return true;
    }
    
    /**
     * Get real client IP
     */
    private function get_client_ip() {
        $ip_headers = array(
            'HTTP_CF_CONNECTING_IP',
            'HTTP_CLIENT_IP',
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_FORWARDED',
            'HTTP_X_CLUSTER_CLIENT_IP',
            'HTTP_FORWARDED_FOR',
            'HTTP_FORWARDED',
            'REMOTE_ADDR'
        );
        
        foreach ($ip_headers as $header) {
            if (!empty($_SERVER[$header])) {
                $ip = $_SERVER[$header];
                // Handle comma-separated IPs
                if (strpos($ip, ',') !== false) {
                    $ip = trim(explode(',', $ip)[0]);
                }
                if (filter_var($ip, FILTER_VALIDATE_IP)) {
                    return $ip;
                }
            }
        }
        
        return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
    }
    
    /**
     * Block request and log
     */
    private function block_request($reason, $code = 403) {
        // error_log('VMPFence EARLY BLOCK: ' . $reason . ' - IP: ' . $this->get_client_ip() . ' - URI: ' . $_SERVER['REQUEST_URI']);
        
        http_response_code($code);
        header('Content-Type: text/html; charset=utf-8');
        
        $code_text = ($code === 429) ? '429 Too Many Requests' : '403 Forbidden';
        
        echo '<!DOCTYPE html>
<html>
<head>
    <title>' . $code_text . '</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 50px; background: #f5f5f5; }
        .container { max-width: 600px; margin: 0 auto; background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { color: #d32f2f; margin-top: 0; }
        .warning { background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0; }
    </style>
</head>
<body>
    <div class="container">
        <h1>⛔ Request Blocked</h1>
        <div class="warning">
            <strong>Your request has been blocked by VMPFence security.</strong>
            <p>Reason: ' . htmlspecialchars($reason) . '</p>
        </div>
        <p><strong>Time:</strong> ' . date('Y-m-d H:i:s') . '</p>
        <p><strong>Code:</strong> ' . $code_text . '</p>
        <hr>
        <p style="font-size: 12px; color: #666;">If you believe this is an error, please contact the site administrator.</p>
    </div>
</body>
</html>';
        exit;
    }
}

// Initialize
VMPFence_Early_Protection::get_instance();
