<?php

/**
 * JCOGS Image Pro - Security Validation Service
 * =============================================
 * Comprehensive security validation for tag parameters
 * 
 * @category   ExpressionEngine Add-on
 * @package    JCOGS Image Pro
 * @author     JCOGS Design <contact@jcogs.net>
 * @copyright  Copyright (c) 2021 - 2025 JCOGS Design
 * @license    https://jcogs.net/add-ons/license/jcogs_img_pro
 * @version    2.0.6
 * @link       https://JCOGS.net/
 * @since      2.0.0
 */

namespace JCOGSDesign\JCOGSImagePro\Service;

/**
 * Security Validation Service
 * 
 * Provides comprehensive security validation for both tag parameters 
 * and variable modifier parameters, protecting against XSS, path traversal,
 * command injection, and other attack vectors.
 */
class SecurityValidationService
{
    /**
     * Check for potentially malicious content in parameter values
     * 
     * @param string $value Parameter value to check
     * @return bool True if content appears malicious
     */
    public function containsMaliciousContent($value): bool
    {
        // First check original value for encoded attacks
        if ($this->checkRawPatterns($value)) {
            return true;
        }
        
        // Decode and normalize the value for comprehensive checking
        $decodedValue = $this->decodeAndNormalize($value);
        
        // Basic checks for script injection, path traversal, etc.
        $dangerous_patterns = [
            // Script injection patterns
            '/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/mi',
            '/javascript:/i',
            '/vbscript:/i',
            '/on\w+\s*=/i',
            '/data:\s*text\/html/i',
            
            // Path traversal patterns (enhanced)
            '/\.\.\//i',
            '/\.\.\\\/i',
            '/\.\.\\\\/i',
            '/\.\.%2f/i',
            '/\.\.%5c/i',
            '/file:\/\//i',
            '/\x2e\x2e\x2f/i',  // UTF-8 encoded dots and slash
            '/\x{002e}\x{002e}\x{002f}/ui', // Unicode escaped (PCRE2 syntax)
            
            // Command injection patterns (enhanced)
            '/\0/',
            '/eval\s*\(/i',
            '/exec\s*\(/i',
            '/system\s*\(/i',
            '/shell_exec\s*\(/i',
            '/passthru\s*\(/i',
            '/`[^`]*`/',
            '/\$\([^)]*\)/',
            '/\${[^}]*}/',
            '/[;&]/',  // Semicolon and ampersand 
            
            // Redirection and pipe operators (context-aware detection)
            '/\w+\s*[<>]\s*\w+/',     // word<word or word>word (redirection)
            '/[<>]{1,2}\s*\//',       // < or > followed by path
            '/\|\s*[a-z]\w*/i',       // pipe followed by command
            '/\|\|\s*[a-z]\w*/i',     // logical OR followed by command
            '/&&\s*[a-z]\w*/i',       // logical AND followed by command
            
            // HTML tag patterns (more specific to avoid false positives with legitimate filenames)
            '/<[a-z][a-z0-9]*(?:\s[^>]*)?>/i',  // Opening HTML tags (require at least 1 letter followed by alphanumeric)
            '/<\/[a-z][a-z0-9]*\s*>/i',         // Closing HTML tags  
            '/<\s+[a-z]/i',                     // HTML tag with leading whitespace (suspicious)
            
            // Quoted command injection patterns
            '/"[^"]*[;&|][^"]*"/',
            "/'[^']*[;&|][^']*'/",
            '/\w+["\'][^"\']*["\']/',  // Split quoted commands like c"a"t
            '/[a-z]+["\']\w+["\']\s+/', // Command with quoted letters
            
            // Advanced patterns
            '/powershell/i',
            '/cmd\.exe/i',
            '/expression\s*\(/i',
            '/@import/i',
            '/url\s*\(/i',
        ];
        
        foreach ($dangerous_patterns as $pattern) {
            if (preg_match($pattern, $decodedValue)) {
                return true;
            }
        }
        
        return false;
    }

    /**
     * Check if a parameter key is a known EE image processing parameter
     * that uses pipe syntax for legitimate purposes
     * 
     * @param string $key Parameter key
     * @return bool True if this is an EE image parameter that uses pipes
     */
    public function isEEImageParameter($key): bool
    {
        $ee_image_params_with_pipes = [
            'crop',         // e.g., "yes|center,center|0,0|yes" 
            'focal_point',  // e.g., "center,center|smart"
            'watermark',    // May use pipe-separated values
        ];
        
        return in_array($key, $ee_image_params_with_pipes);
    }
    
    /**
     * Check for malicious content in EE image processing parameters
     * Uses relaxed validation that allows pipes but still catches real threats
     * 
     * @param string $value Parameter value to check
     * @return bool True if content appears malicious
     */
    public function containsMaliciousContentForEEParams($value): bool
    {
        // First check original value for encoded attacks
        if ($this->checkRawPatterns($value)) {
            return true;
        }
        
        // Decode and normalize the value
        $decodedValue = $this->decodeAndNormalize($value);
        
        // Relaxed patterns that exclude pipes but catch real threats
        $dangerous_patterns = [
            // Script injection patterns
            '/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/mi',
            '/javascript:/i',
            '/vbscript:/i',
            '/on\w+\s*=/i',
            '/data:\s*text\/html/i',
            
            // Path traversal patterns 
            '/\.\.\//i',
            '/\.\.\\\/i',
            '/\.\.\\\\/i',
            '/\.\.%2f/i',
            '/\.\.%5c/i',
            '/file:\/\//i',
            
            // Command injection patterns (excluding pipes and semicolons)
            '/\0/',
            '/eval\s*\(/i',
            '/exec\s*\(/i',
            '/system\s*\(/i',
            '/shell_exec\s*\(/i',
            '/passthru\s*\(/i',
            '/`[^`]*`/',
            '/\$\([^)]*\)/',
            '/\${[^}]*}/',
            // Note: [;&|] patterns removed for EE params
            
            // Redirection operators (but not pipes)
            '/\w+\s*[<>]\s*\w+/',     
            '/[<>]{1,2}\s*\//',       
            '/\|\|\s+[a-z]\w*/i',     // Still catch logical OR (requires whitespace after ||)
            '/&&\s+[a-z]\w*/i',       // Still catch logical AND (requires whitespace after &&)
            
            // HTML tag patterns
            '/<[a-z][a-z0-9]*(?:\s[^>]*)?>/i',
            '/<\/[a-z][a-z0-9]*\s*>/i',
            '/<\s+[a-z]/i',
            
            // Advanced patterns
            '/powershell/i',
            '/cmd\.exe/i',
            '/expression\s*\(/i',
            '/@import/i',
            '/url\s*\(/i',
        ];
        
        foreach ($dangerous_patterns as $pattern) {
            if (preg_match($pattern, $decodedValue)) {
                return true;
            }
        }

        return false;
    }
    
    /**
     * Check for malicious content in CSS style attributes
     * 
     * This method validates CSS content while allowing legitimate CSS syntax
     * like semicolons, colons, and common CSS properties. It specifically
     * looks for JavaScript injection attempts and other dangerous patterns.
     * 
     * Note: Path traversal (../) is permitted in CSS as relative URLs in
     * url() functions are resolved by the browser relative to the stylesheet,
     * not the server filesystem, so they pose no security risk.
     * 
     * @param string $value CSS style attribute value to check
     * @return bool True if content appears malicious
     */
    public function containsMaliciousCSSContent($value): bool
    {
        // First check original value for encoded attacks
        if ($this->checkRawPatterns($value)) {
            return true;
        }
        
        // Decode and normalize the value
        $decodedValue = $this->decodeAndNormalize($value);
        
        // CSS-specific dangerous patterns
        $dangerous_css_patterns = [
            // JavaScript execution attempts
            '/javascript:/i',
            '/vbscript:/i',
            '/expression\s*\(/i',           // IE expression() vulnerability
            '/behavior\s*:/i',              // IE behavior property
            '/-moz-binding\s*:/i',          // Firefox XBL binding
            '/import\s+["\']?javascript:/i',
            
            // Event handlers (not valid in inline styles but check anyway)
            '/on\w+\s*=/i',
            
            // URL with JavaScript
            '/url\s*\(\s*["\']?\s*javascript:/i',
            '/url\s*\(\s*["\']?\s*data:\s*text\/html/i',
            
            // Script tags (shouldn't be in CSS but check anyway)
            '/<script\b/i',
            '/<\/script>/i',
            
            // Command injection patterns (for completeness)
            '/\x00/',                       // Null bytes
            // Note: Path traversal (../) is NOT checked for CSS - it's safe in CSS url()
            '/\$\{/i',                      // Variable substitution
            '/`[^`]*`/',                    // Backticks
        ];
        
        foreach ($dangerous_css_patterns as $pattern) {
            if (preg_match($pattern, $decodedValue)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Check raw patterns before decoding
     * 
     * @param string $value Original parameter value
     * @return bool True if raw patterns match malicious content
     */
    private function checkRawPatterns($value): bool
    {
        $raw_patterns = [
            '/\x00/',           // Direct null byte
            '/%00/',            // URL encoded null
            '/\\\\0/',          // Escaped null  
            '/\\\u0000/',       // Unicode null
            '/\xc0\x80/',       // Overlong UTF-8 null
            '/%c0%af/',         // UTF-8 encoded slash (overlong encoding)
            '/\w+["\'][^"\']*["\'][^"\']*/',  // Quoted command patterns like c"a"t
        ];
        
        foreach ($raw_patterns as $pattern) {
            if (preg_match($pattern, $value)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Decode and normalize input for comprehensive security checking
     * 
     * @param string $value Input value to decode
     * @return string Decoded and normalized value
     */
    public function decodeAndNormalize($value): string
    {
        // Start with original value
        $decoded = $value;
        
        // URL decode (multiple rounds to catch double encoding)
        for ($i = 0; $i < 3; $i++) {
            $newDecoded = urldecode($decoded);
            if ($newDecoded === $decoded) {
                break; // No more changes
            }
            $decoded = $newDecoded;
        }
        
        // HTML entity decode
        $decoded = html_entity_decode($decoded, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        
        // Unicode normalization for UTF-8 attacks
        if (function_exists('normalizer_normalize')) {
            $decoded = normalizer_normalize($decoded, \Normalizer::FORM_KC);
        }
        
        // Handle various null byte encodings before normalization
        $patterns = [
            '/\x00/',           // Actual null byte
            '/%00/',            // URL encoded null
            '/\\\\0/',          // Escaped null
            '/\\\u0000/',       // Unicode escaped null
            '/\0/',             // PHP null representation
        ];
        
        foreach ($patterns as $pattern) {
            $decoded = preg_replace($pattern, '', $decoded);
        }
        
        // Convert to lowercase for case-insensitive matching
        $normalized = strtolower($decoded);
        
        // Remove common obfuscation characters but preserve structure for quoted strings
        $normalized = str_replace(["\r", "\n", "\t"], '', $normalized);
        
        return $normalized;
    }
    
    /**
     * Validate and sanitize an array of parameters
     * 
     * @param array $parameters Parameters to validate and sanitize
     * @return array Sanitized parameters with malicious content removed/escaped
     */
    public function validateAndSanitizeParameters(array $parameters): array
    {
        $sanitized = [];
        
        foreach ($parameters as $key => $value) {
            $key_lower = strtolower(trim((string) $key));

            // Validate parameter key itself
            if ($this->containsMaliciousContent($key)) {
                // Skip malicious parameter keys entirely
                continue;
            }
            
            // Validate and sanitize parameter value
            if (is_string($value)) {
                // Special handling for style attribute - allows CSS syntax
                if ($key_lower === 'style') {
                    if ($this->containsMaliciousCSSContent($value)) {
                        // Malicious CSS detected - remove parameter
                        continue;
                    } else {
                        // Safe CSS - apply basic normalization
                        $sanitized[$key] = $this->basicSanitize($value);
                    }
                } else if (
                    // Known Image Pro parameters (registered in ParameterRegistry) and EE-style
                    // image params should use relaxed validation.
                    //
                    // Rationale: many Image Pro parameters legitimately use special characters
                    // (pipes, commas, parentheses) and are validated later by the registry/
                    // package validators. Unknown keys are treated as passthrough HTML
                    // attributes and must remain under strict validation.
                    ParameterRegistry::getParameterDefinition($key_lower) !== null
                    || $this->isEEImageParameter($key_lower)
                ) {
                    // Image processing parameters - use relaxed validation
                    if ($this->containsMaliciousContentForEEParams($value)) {
                        // Malicious content in EE param - remove parameter
                        continue;
                    } else {
                        // Safe EE parameter - apply basic normalization
                        $sanitized[$key] = $this->basicSanitize($value);
                    }
                } else {
                    // Standard parameter validation
                    if ($this->containsMaliciousContent($value)) {
                        // For malicious content, either remove or escape
                        // For safety, we'll remove the parameter entirely
                        continue;
                    } else {
                        // Safe content - apply basic normalization
                        $sanitized[$key] = $this->basicSanitize($value);
                    }
                }
            } else {
                // Non-string values (arrays, objects) - pass through if key is safe
                $sanitized[$key] = $value;
            }
        }
        
        return $sanitized;
    }
    
    /**
     * Apply basic sanitization to safe content
     * 
     * @param string $value Safe value to sanitize
     * @return string Sanitized value
     */
    private function basicSanitize($value): string
    {
        // Remove any null bytes
        $sanitized = str_replace(["\0", "\x00"], '', $value);
        
        // Trim whitespace
        $sanitized = trim($sanitized);
        
        return $sanitized;
    }
}
