<?php

declare(strict_types=1);
// Pure PHP pre-WordPress WAF bootstrap. Do not call WordPress APIs here.

if ( ! defined( 'ABSPATH' ) ) {
    $sitefort_waf_loaded_inside_wordpress = false;
    $sitefort_waf_bootstrap_script = (string) filter_input( INPUT_SERVER, 'SCRIPT_FILENAME', FILTER_UNSAFE_RAW );
    $sitefort_waf_bootstrap_path = realpath( __FILE__ );
    if (
        is_string( $sitefort_waf_bootstrap_script )
        && $sitefort_waf_bootstrap_path !== false
        && realpath( $sitefort_waf_bootstrap_script ) === $sitefort_waf_bootstrap_path
    ) {
        http_response_code( 404 );
        exit;
    }
    unset( $sitefort_waf_bootstrap_path );
} else {
    $sitefort_waf_loaded_inside_wordpress = true;
}

if ( defined( 'SITEFORT_WAF_LOADED' ) ) {
    unset( $sitefort_waf_loaded_inside_wordpress );
    return;
}
define( 'SITEFORT_WAF_LOADED', true );

if ( ! function_exists( 'str_contains' ) ) {
    function str_contains( string $haystack, string $needle ): bool {
        return $needle === '' || strpos( $haystack, $needle ) !== false;
    }
}

if ( ! function_exists( 'str_starts_with' ) ) {
    function str_starts_with( string $haystack, string $needle ): bool {
        return $needle === '' || strncmp( $haystack, $needle, strlen( $needle ) ) === 0;
    }
}

if ( ! function_exists( 'str_ends_with' ) ) {
    function str_ends_with( string $haystack, string $needle ): bool {
        return $needle === '' || substr( $haystack, -strlen( $needle ) ) === $needle;
    }
}

$sitefort_waf_runtime_storage_file = defined( 'SITEFORT_WAF_RUNTIME_STORAGE_FILE' )
    ? (string) constant( 'SITEFORT_WAF_RUNTIME_STORAGE_FILE' )
    : __DIR__ . '/BootstrapRuntimeStorage.php';
if ( ! is_file( $sitefort_waf_runtime_storage_file ) ) {
    $sitefort_waf_candidate = dirname( __DIR__ ) . '/plugins/sitefort/modules/Firewall/ExtendedProtection/BootstrapRuntimeStorage.php';
    if ( is_file( $sitefort_waf_candidate ) ) {
        $sitefort_waf_runtime_storage_file = $sitefort_waf_candidate;
    }
    unset( $sitefort_waf_candidate );
}

if ( is_file( $sitefort_waf_runtime_storage_file ) && is_readable( $sitefort_waf_runtime_storage_file ) ) {
    require_once $sitefort_waf_runtime_storage_file;
}

unset( $sitefort_waf_runtime_storage_file );

function sitefort_waf_server_value( string $key ): string {
    $value = filter_input( INPUT_SERVER, $key, FILTER_UNSAFE_RAW );
    return is_string( $value ) ? str_replace( "\0", '', $value ) : '';
}

function sitefort_waf_request_uri_component( string $request_uri, string $component ): string {
    $uri = explode( '#', $request_uri, 2 )[0];
    $pos = strpos( $uri, '?' );

    if ( $component === 'query' ) {
        return $pos === false ? '' : (string) substr( $uri, $pos + 1 );
    }

    $path = $pos === false ? $uri : (string) substr( $uri, 0, $pos );
    return $path !== '' ? $path : '/';
}

function sitefort_waf_is_activation_probe_query( string $query ): bool {
    if ( $query === '' ) {
        return false;
    }

    $params = [];
    parse_str( $query, $params );

    return isset( $params['sitefort_waf_activation_probe'] ) && (string) $params['sitefort_waf_activation_probe'] === '1';
}

function sitefort_waf_is_xmlrpc_path( string $path ): bool {
    $normalized = preg_replace( '#/+#', '/', str_replace( '\\', '/', $path ) );
    if ( ! is_string( $normalized ) ) {
        $normalized = $path;
    }

    return strtolower( basename( $normalized ) ) === 'xmlrpc.php';
}

function sitefort_waf_normalize_request_path( string $path ): string {
    $path = rawurldecode( trim( $path ) );
    if ( $path === '' ) {
        return '/';
    }

    $path = str_replace( '\\', '/', $path );
    $path = preg_replace( '#/+#', '/', $path ) ?? $path;
    $path = '/' . ltrim( $path, '/' );

    return $path === '/' ? '/' : rtrim( $path, '/' );
}

function sitefort_waf_sensitive_path_matches( array $policy ): bool {
    if ( empty( $policy['sensitive_path_enabled'] ) || ! is_array( $policy['sensitive_path_patterns'] ?? null ) ) {
        return false;
    }

    $path = sitefort_waf_normalize_request_path(
        sitefort_waf_request_uri_component(
            sitefort_waf_server_value( 'REQUEST_URI' ),
            'path'
        )
    );

    foreach ( $policy['sensitive_path_patterns'] as $pattern ) {
        if ( ! is_string( $pattern ) || $pattern === '' || strlen( $pattern ) > 1024 || strpos( $pattern, "\0" ) !== false ) {
            continue;
        }

        if ( @preg_match( $pattern, $path ) === 1 ) {
            return true;
        }
    }

    return false;
}

/**
 * Verify whether the current request is a signed internal SiteFort
 * server-verification probe.
 *
 * This must run before normal WAF policy evaluation so SiteFort does not block
 * its own hardening probes on hosts where loopback requests arrive through the
 * public origin path instead of pure localhost.
 *
 * The signature binds the HTTP method, path, query string, nonce, and
 * timestamp to a secret written into the generated WAF config. A bounded
 * nonce cache prevents replay inside the validity window.
 *
 * @param array  $config   WAF config array.
 * @param string $data_dir SiteFort WAF data directory.
 * @return bool
 */
function sitefort_waf_is_signed_server_verification_request( array $config, string $data_dir ): bool {
    $verification = is_array( $config['internal_verification'] ?? null ) ? $config['internal_verification'] : [];
    $version      = (string) ( $verification['version'] ?? '' );
    $window       = max( 1, (int) ( $verification['window'] ?? 120 ) );
    $secret       = (string) ( $verification['secret'] ?? '' );

    if ( $version !== '1' || $secret === '' ) {
        return false;
    }

    $header_version   = sitefort_waf_server_value( 'HTTP_X_SITEFORT_VERIFY_VERSION' );
    $header_timestamp = sitefort_waf_server_value( 'HTTP_X_SITEFORT_VERIFY_TIMESTAMP' );
    $header_nonce     = sitefort_waf_server_value( 'HTTP_X_SITEFORT_VERIFY_NONCE' );
    $header_signature = sitefort_waf_server_value( 'HTTP_X_SITEFORT_VERIFY_SIGNATURE' );

    if ( $header_version !== '1' || $header_timestamp === '' || $header_nonce === '' || $header_signature === '' ) {
        return false;
    }

    if (
        ! ctype_digit( $header_timestamp )
        || strlen( $header_nonce ) > 128
        || strlen( $header_signature ) > 128
    ) {
        return false;
    }

    $timestamp = (int) $header_timestamp;
    if ( abs( time() - $timestamp ) > $window ) {
        return false;
    }

    $request_uri = sitefort_waf_server_value( 'REQUEST_URI' );
    $path        = sitefort_waf_request_uri_component( $request_uri, 'path' );
    $query       = sitefort_waf_request_uri_component( $request_uri, 'query' );
    $method      = strtoupper( sitefort_waf_server_value( 'REQUEST_METHOD' ) ?: 'GET' );
    $expected    = hash_hmac(
        'sha256',
        $method . '|' . $path . '|' . $query . '|' . $header_nonce . '|' . $timestamp,
        $secret
    );

    if ( ! hash_equals( $expected, $header_signature ) ) {
        return false;
    }

    return sitefort_waf_mark_verify_nonce_used( $config, $data_dir, $header_nonce, $header_signature, $timestamp, $window );
}

/**
 * Mark a server-verification nonce as used.
 *
 * @param array  $config
 * @param string $data_dir
 * @param string $nonce
 * @param string $signature
 * @param int    $timestamp
 * @param int    $window
 * @return bool
 */
function sitefort_waf_mark_verify_nonce_used( array $config, string $data_dir, string $nonce, string $signature, int $timestamp, int $window ): bool {
    if ( function_exists( 'sitefort_waf_db_mark_verification_nonce' ) ) {
        $db_result = sitefort_waf_db_mark_verification_nonce( $config, $data_dir, $nonce, $signature, $timestamp, $window );
        if ( is_bool( $db_result ) ) {
            return $db_result;
        }
    }

    $guard = "<?php exit; ?>\n";
    $file  = $data_dir . '/waf-verify-nonces.php';
    $key   = hash( 'sha256', $nonce . '|' . $signature . '|' . $timestamp );
    $now   = time();
    $cutoff = $now - $window;

    if ( ! is_file( $file ) ) {
        @file_put_contents( $file, $guard . json_encode( [] ), LOCK_EX );
    }

    $handle = @fopen( $file, 'c+' );
    if ( $handle === false ) {
        return false;
    }

    $result = false;

    if ( flock( $handle, LOCK_EX ) ) {
        $raw = stream_get_contents( $handle );
        if ( is_string( $raw ) && strncmp( $raw, $guard, strlen( $guard ) ) === 0 ) {
            $raw = substr( $raw, strlen( $guard ) );
        }

        $data = @json_decode( is_string( $raw ) ? $raw : '', true );
        if ( ! is_array( $data ) ) {
            $data = [];
        }

        $fresh = [];
        foreach ( $data as $stored_key => $stored_ts ) {
            $stored_key = is_string( $stored_key ) ? $stored_key : '';
            $stored_ts  = is_int( $stored_ts ) ? $stored_ts : (int) $stored_ts;
            if ( $stored_key !== '' && $stored_ts >= $cutoff ) {
                $fresh[ $stored_key ] = $stored_ts;
            }
        }

        if ( ! isset( $fresh[ $key ] ) ) {
            $fresh[ $key ] = $now;
            $payload = $guard . json_encode( $fresh );
            if ( is_string( $payload ) ) {
                rewind( $handle );
                ftruncate( $handle, 0 );
                $written = fwrite( $handle, $payload );
                fflush( $handle );
                $result = is_int( $written ) && $written === strlen( $payload );
            }
        }

        flock( $handle, LOCK_UN );
    }

    fclose( $handle );

    return $result;
}

$sitefort_waf_data_dir = ( defined( 'SITEFORT_WAF_DATA_DIR' ) && is_string( constant( 'SITEFORT_WAF_DATA_DIR' ) ) )
    ? (string) constant( 'SITEFORT_WAF_DATA_DIR' )
    : dirname( __FILE__ );
$sitefort_waf_config_file = $sitefort_waf_data_dir . '/waf-config.php';
/** @var array|false $sitefort_waf_config */
$sitefort_waf_config = false;

if ( is_file( $sitefort_waf_config_file ) && is_readable( $sitefort_waf_config_file ) ) {
    $sitefort_waf_config = @include $sitefort_waf_config_file;
}

if (
    ! is_array( $sitefort_waf_config )
    && function_exists( 'sitefort_waf_runtime_storage_forced_mysqli' )
    && function_exists( 'sitefort_waf_db_runtime_payload' )
    && sitefort_waf_runtime_storage_forced_mysqli()
) {
    $sitefort_waf_seed_config = [
        'runtime_storage_backend' => 'mysqli',
        'db_table_prefix' => '',
        'blog_id' => 1,
        'internal_verification' => [
            'nonce_site_token' => '',
        ],
    ];

    $sitefort_waf_state_config = sitefort_waf_db_runtime_payload(
        $sitefort_waf_seed_config,
        $sitefort_waf_data_dir,
        'bootstrap_config',
        null
    );

    if ( is_array( $sitefort_waf_state_config ) ) {
        $sitefort_waf_config = $sitefort_waf_state_config;
    }
}

if ( ! is_array( $sitefort_waf_config ) ) {
    // No config yet -- nothing to enforce. Let the request through.
    return;
}

if (
    function_exists( 'sitefort_waf_db_runtime_payload' )
    && function_exists( 'sitefort_waf_runtime_uses_mysqli' )
    && sitefort_waf_runtime_uses_mysqli( $sitefort_waf_config )
) {
    $sitefort_waf_state_config = sitefort_waf_db_runtime_payload(
        $sitefort_waf_config,
        $sitefort_waf_data_dir,
        'bootstrap_config',
        null
    );

    if ( is_array( $sitefort_waf_state_config ) ) {
        $sitefort_waf_config = $sitefort_waf_state_config;
    }
}

$sitefort_waf_policy_signature = (string) ( $sitefort_waf_config['policy_signature'] ?? '' );
if ( $sitefort_waf_policy_signature !== '' && ! defined( 'SITEFORT_WAF_POLICY_SIGNATURE' ) ) {
    define( 'SITEFORT_WAF_POLICY_SIGNATURE', $sitefort_waf_policy_signature );
}

$sitefort_waf_request_uri = sitefort_waf_server_value( 'REQUEST_URI' );
$sitefort_waf_request_query = sitefort_waf_request_uri_component( $sitefort_waf_request_uri, 'query' );
$sitefort_waf_activation_probe = (
    sitefort_waf_is_activation_probe_query( $sitefort_waf_request_query )
    && ! $sitefort_waf_loaded_inside_wordpress
    && sitefort_waf_is_signed_server_verification_request( $sitefort_waf_config, $sitefort_waf_data_dir )
);

$sitefort_waf_origin_mode = (string) ( $sitefort_waf_config['origin_enforcement_mode'] ?? 'pending_validation' );
if ( $sitefort_waf_origin_mode !== 'enforce' ) {
    return;
}

$sitefort_waf_engine_file = (string) ( $sitefort_waf_config['engine_file'] ?? '' );
if ( $sitefort_waf_engine_file === '' || ! is_file( $sitefort_waf_engine_file ) || ! is_readable( $sitefort_waf_engine_file ) ) {
    return;
}

$sitefort_waf_search_trust_core_file = (string) ( $sitefort_waf_config['search_trust_core_file'] ?? '' );
$sitefort_waf_search_trust_context_file = (string) ( $sitefort_waf_config['search_trust_context_file'] ?? '' );
$sitefort_waf_search_trust_dns_verifier_file = (string) ( $sitefort_waf_config['search_trust_dns_verifier_file'] ?? '' );
$sitefort_waf_request_provenance_file = (string) ( $sitefort_waf_config['request_provenance_file'] ?? '' );

if ( ! defined( 'SITEFORT_STANDALONE_BOOTSTRAP' ) ) {
    define( 'SITEFORT_STANDALONE_BOOTSTRAP', true );
}

foreach (
    [
        $sitefort_waf_search_trust_core_file,
        $sitefort_waf_search_trust_context_file,
        $sitefort_waf_request_provenance_file,
        $sitefort_waf_engine_file,
        $sitefort_waf_search_trust_dns_verifier_file,
    ] as $sitefort_waf_required_file
) {
    if ( $sitefort_waf_required_file === '' || ! is_file( $sitefort_waf_required_file ) || ! is_readable( $sitefort_waf_required_file ) ) {
        return;
    }

    require_once $sitefort_waf_required_file;
}

if (
    ! class_exists( '\SiteFort\Modules\Firewall\RuleEngine' )
    || ! class_exists( '\SiteFort\Modules\Firewall\SearchTrust\SearchTrustCore' )
    || ! class_exists( '\SiteFort\Modules\Firewall\SearchTrust\SearchTrustContext' )
    || ! class_exists( '\SiteFort\Modules\Firewall\SearchTrust\SearchTrustDnsVerifier' )
) {
    return;
}

if ( ! defined( 'SITEFORT_WAF_POLICY_APPLIED' ) ) {
    define( 'SITEFORT_WAF_POLICY_APPLIED', true );
}

if ( $sitefort_waf_activation_probe ) {
    header( 'X-SiteFort-WAF-Active: 1' );
    header( 'Content-Type: text/plain; charset=utf-8' );
    http_response_code( 200 );
    echo '1';
    exit;
}
unset( $sitefort_waf_loaded_inside_wordpress, $sitefort_waf_activation_probe );

if ( sitefort_waf_is_signed_server_verification_request( $sitefort_waf_config, $sitefort_waf_data_dir ) ) {
    return;
}


/**
 * Determine the real client IP address.
 *
 * Algorithm mirrors the main IpDetector class. Reads proxy detection
 * settings from the WAF config so the same detection mode, header, and
 * trusted proxy list are used whether WordPress is loaded or not.
 *
 * Key decisions (matching IpDetector):
 *   - REMOTE_ADDR is always the baseline.
 *   - Proxy headers are only read if REMOTE_ADDR is a trusted proxy.
 *   - X-Forwarded-For uses RIGHTMOST public IP (not leftmost).
 *   - Port numbers are stripped before validation.
 *   - IPv6-mapped-IPv4 addresses are normalized.
 *
 * @param array $config The WAF config array.
 * @return string Client IP address.
 */
function sitefort_waf_get_ip( array $config ): string {
    $remote_addr = sitefort_waf_sanitize_ip( sitefort_waf_server_value( 'REMOTE_ADDR' ) );
    if ( ! filter_var( $remote_addr, FILTER_VALIDATE_IP ) ) {
        return '0.0.0.0';
    }

    $ip_config = $config['ip_detection'] ?? [];
    $mode      = $ip_config['mode'] ?? 'automatic';
    $cf_ranges = $ip_config['cloudflare_ranges'] ?? [];

    // Mode: disabled. Only use the TCP connection IP.
    if ( 'disabled' === $mode ) {
        return sitefort_waf_normalize_ip( $remote_addr );
    }

    // Only read proxy headers if REMOTE_ADDR belongs to a trusted proxy.
    $trusted = $ip_config['trusted_proxies'] ?? [];
    if ( empty( $trusted ) && defined( 'SITEFORT_TRUSTED_PROXIES' ) ) {
        $trusted = (array) SITEFORT_TRUSTED_PROXIES;
    }

    // Zero-config Cloudflare parity with runtime RequestProvenance.
    $provenance = sitefort_waf_resolve_request_provenance( $config );
    if (
        ! empty( $provenance['cloudflare_peer_verified'] )
        && (
            'automatic' === $mode
            || ( 'manual' === $mode && ( $ip_config['header'] ?? '' ) === 'HTTP_CF_CONNECTING_IP' )
        )
    ) {
        $cf_ip = sitefort_waf_extract_ip( 'HTTP_CF_CONNECTING_IP', [] );
        if ( '' !== $cf_ip ) {
            return sitefort_waf_normalize_ip( $cf_ip );
        }
    }

    if ( empty( $trusted ) ) {
        return sitefort_waf_normalize_ip( $remote_addr );
    }

    $is_trusted = false;
    foreach ( $trusted as $proxy ) {
        if ( sitefort_waf_ip_matches( $remote_addr, (string) $proxy ) ) {
            $is_trusted = true;
            break;
        }
    }

    if ( ! $is_trusted ) {
        return sitefort_waf_normalize_ip( $remote_addr );
    }

    // Supported proxy headers in priority order.
    $all_headers = [
        'HTTP_CF_CONNECTING_IP',
        'HTTP_X_FORWARDED_FOR',
        'HTTP_FORWARDED',
        'HTTP_X_REAL_IP',
        'HTTP_X_CLIENT_IP',
        'HTTP_CLIENT_IP',
        'HTTP_X_CLUSTER_CLIENT_IP',
    ];

    // Mode: manual. Only read the configured header.
    if ( 'manual' === $mode ) {
        $header = $ip_config['header'] ?? '';
        if ( ! empty( $header ) && in_array( $header, $all_headers, true ) ) {
            $ip = sitefort_waf_extract_ip( $header, $trusted );
            if ( '' !== $ip ) {
                return sitefort_waf_normalize_ip( $ip );
            }
        }
        return sitefort_waf_normalize_ip( $remote_addr );
    }

    // Mode: automatic. Iterate all headers in priority order.
    foreach ( $all_headers as $header ) {
        $ip = sitefort_waf_extract_ip( $header, $trusted );
        if ( '' !== $ip ) {
            return sitefort_waf_normalize_ip( $ip );
        }
    }

    return sitefort_waf_normalize_ip( $remote_addr );
}

/**
 * Resolve the request country code from trusted Cloudflare headers.
 *
 * The standalone bootstrap does not perform local MMDB lookups. Country
 * enforcement is only available here when the request is known to be coming
 * through Cloudflare and CF-IPCountry is present.
 *
 * @param array $config The WAF config array.
 * @return string ISO 3166-1 alpha-2 country code or '' when unavailable.
 */
function sitefort_waf_get_country_code( array $config ): string {
    $provenance = sitefort_waf_resolve_request_provenance( $config );
    if ( empty( $provenance['cloudflare_peer_verified'] ) || empty( $provenance['cloudflare_country_valid'] ) ) {
        return '';
    }

    return strtoupper( trim( (string) ( $provenance['cf_ipcountry'] ?? '' ) ) );
}

/**
 * Resolve request provenance using the shared runtime/bootstrap contract.
 *
 * @param array $config The WAF config array.
 * @return array<string,mixed>
 */
function sitefort_waf_resolve_request_provenance( array $config ): array {
    static $cache = null;

    if ( is_array( $cache ) ) {
        return $cache;
    }

    if ( ! class_exists( '\SiteFort\Includes\RequestProvenance' ) ) {
        return [];
    }

    $ip_config = is_array( $config['ip_detection'] ?? null ) ? $config['ip_detection'] : [];
    $cache = \SiteFort\Includes\RequestProvenance::resolve_from_server(
        $_SERVER,
        [
            'cloudflare_ranges' => is_array( $ip_config['cloudflare_ranges'] ?? null ) ? $ip_config['cloudflare_ranges'] : [],
        ]
    );

    return $cache;
}

/**
 * Extract the best IP from a proxy header.
 *
 * For comma-separated headers (X-Forwarded-For), walks RIGHT-to-LEFT
 * skipping trusted proxies, returning the first public IP. Falls back
 * to the first valid private IP if no public one is found.
 *
 * @param string $header  $_SERVER key.
 * @param array  $trusted Trusted proxy list (IPs and/or CIDRs).
 * @return string Validated IP or ''.
 */
function sitefort_waf_extract_ip( string $header, array $trusted ): string {
    $raw = sitefort_waf_server_value( $header );
    if ( empty( $raw ) || ! is_string( $raw ) ) {
        return '';
    }

    // RFC 7239 Forwarded header.
    if ( 'HTTP_FORWARDED' === $header ) {
        if ( preg_match_all( '/for=["\']?\[?([a-f0-9.:]+)\]?["\']?/i', $raw, $m ) ) {
            $raw = implode( ',', $m[1] );
        } else {
            return '';
        }
    }

    // Comma-separated chain.
    if ( strpos( $raw, ',' ) !== false ) {
        $parts            = array_map( 'trim', explode( ',', $raw ) );
        $fallback_private = '';

        for ( $i = count( $parts ) - 1; $i >= 0; $i-- ) {
            $ip = sitefort_waf_sanitize_ip( $parts[ $i ] );
            if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
                continue;
            }
            // Skip trusted proxies.
            $skip = false;
            foreach ( $trusted as $t ) {
                if ( sitefort_waf_ip_matches( $ip, (string) $t ) ) {
                    $skip = true;
                    break;
                }
            }
            if ( $skip ) {
                continue;
            }
            if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
                return $ip;
            }
            if ( '' === $fallback_private ) {
                $fallback_private = $ip;
            }
        }

        return $fallback_private;
    }

    // Single value.
    $ip = sitefort_waf_sanitize_ip( $raw );
    if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
        return sitefort_waf_normalize_cloudflare_header_ip( $header, $ip );
    }

    return '';
}

/**
 * Prefer CF-Connecting-IPv6 when Cloudflare overwrote CF-Connecting-IP with
 * a pseudo IPv4 address.
 *
 * @param string $header
 * @param string $ip
 * @return string
 */
function sitefort_waf_normalize_cloudflare_header_ip( string $header, string $ip ): string {
    if ( 'HTTP_CF_CONNECTING_IP' !== $header || ! sitefort_waf_is_cloudflare_pseudo_ipv4( $ip ) ) {
        return $ip;
    }

    $ipv6 = sitefort_waf_sanitize_ip( sitefort_waf_server_value( 'HTTP_CF_CONNECTING_IPV6' ) );
    if ( filter_var( $ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) === false ) {
        return $ip;
    }

    return $ipv6;
}

/**
 * Detect Cloudflare Pseudo IPv4 overwrite addresses (Class E 240.0.0.0/4).
 *
 * @param string $ip
 * @return bool
 */
function sitefort_waf_is_cloudflare_pseudo_ipv4( string $ip ): bool {
    return filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) !== false
        && sitefort_waf_ip_matches( $ip, '240.0.0.0/4' );
}

/**
 * Strip port from an IP string and trim whitespace.
 *
 * @param string $ip Raw IP value.
 * @return string Cleaned IP.
 */
function sitefort_waf_sanitize_ip( string $ip ): string {
    $ip = trim( $ip );
    if ( '' === $ip ) {
        return '';
    }
    // IPv6 in brackets: [::1]:port
    if ( $ip[0] === '[' ) {
        $close = strpos( $ip, ']' );
        if ( false !== $close ) {
            $ip = substr( $ip, 1, $close - 1 );
        }
    } elseif ( strpos( $ip, '.' ) !== false && substr_count( $ip, ':' ) === 1 ) {
        // IPv4 with port: 1.2.3.4:8080
        $ip = explode( ':', $ip, 2 )[0];
    }
    return $ip;
}

/**
 * Normalize an IP: convert IPv6-mapped-IPv4 to plain IPv4.
 *
 * @param string $ip Validated IP.
 * @return string Normalized IP.
 */
function sitefort_waf_normalize_ip( string $ip ): string {
    return \SiteFort\Modules\Firewall\SearchTrust\SearchTrustCore::normalize_ip( $ip );
}

/**
 * Send a block response and terminate execution.
 *
 * @param string $message Human-readable reason displayed to the visitor.
 * @param int    $code    HTTP status code (403 or 429).
 * @param string $ip      Resolved client IP to display.
 */
function sitefort_waf_block( string $message, int $code = 403, string $ip = '' ): void {
    if ( ! headers_sent() ) {
        http_response_code( $code );
        header( 'Content-Type: text/html; charset=UTF-8' );
        header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );
        if ( $code === 429 ) {
            header( 'Retry-After: 60' );
        }
    }

    // Try to locate block-page.php. When running from the plugin directory,
    // it is one level up. When running as a copied bootstrap in the data dir,
    // walk up to find the plugin's Firewall module directory.
    $block_page = dirname( __DIR__ ) . '/block-page.php';
    if ( ! is_file( $block_page ) ) {
        // Fallback: the data dir lives at wp-content/sitefort-data; the plugin
        // Firewall module is at wp-content/plugins/sitefort/modules/Firewall.
        $sitefort_waf_candidate = dirname( __DIR__, 2 ) . '/plugins/sitefort/modules/Firewall/block-page.php';
        if ( is_file( $sitefort_waf_candidate ) ) {
            $block_page = $sitefort_waf_candidate;
        }
        unset( $sitefort_waf_plugin_dir, $sitefort_waf_candidate );
    }
    if ( is_file( $block_page ) ) {
        require_once $block_page;
        $display_ip = $ip !== '' ? $ip : sitefort_waf_server_value( 'REMOTE_ADDR' );
        sitefort_render_block_page( $code, $message, $display_ip );
    } else {
        // Fallback if template is missing (should not happen in normal installs).
        $status_text = ( $code === 429 ) ? 'Too Many Requests' : 'Forbidden';
        $safe_code   = htmlspecialchars( (string) $code, ENT_QUOTES, 'UTF-8' );
        $safe_msg    = htmlspecialchars( $message, ENT_QUOTES, 'UTF-8' );
        $safe_status = htmlspecialchars( $status_text, ENT_QUOTES, 'UTF-8' );
        // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
        echo '<!DOCTYPE html><html><head><title>' . $safe_code . ' ' . $safe_status . '</title></head><body>';
        echo '<h1>' . $safe_code . ' ' . $safe_status . '</h1>';
        echo '<p>' . $safe_msg . '</p>';
        echo '</body></html>';
        // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
    }
    exit;
}

/**
 * Append a log entry to the WAF block log file.
 *
 * Performs automatic rotation when the log exceeds 1 MB.
 *
 * @param string $data_dir Absolute path to the data directory.
 * @param string $ip       Client IP address.
 * @param string $reason   Block reason.
 */
function sitefort_waf_log( string $data_dir, string $ip, string $reason ): void {
    global $sitefort_waf_config;

    if (
        is_array( $sitefort_waf_config )
        && function_exists( 'sitefort_waf_db_log_event' )
        && function_exists( 'sitefort_waf_runtime_uses_mysqli' )
        && sitefort_waf_runtime_uses_mysqli( $sitefort_waf_config )
        && sitefort_waf_db_log_event(
            $sitefort_waf_config,
            $data_dir,
            $ip,
            sitefort_waf_attack_type_from_reason( $reason ),
            sitefort_waf_http_status_from_reason( $reason )
        )
    ) {
        return;
    }

    $log_file = $data_dir . '/waf-blocks.php';
    $max_size = 1048576;

    // Rotate if the log file exceeds the size limit.
    if ( is_file( $log_file ) && @filesize( $log_file ) > $max_size ) {
        $rotated = substr( $log_file, 0, -4 ) . '.' . gmdate( 'Ymd-His' ) . '.php';
        // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- Standalone pre-WordPress bootstrap rotates its own flat-file log before WordPress is loaded.
        @rename( $log_file, $rotated );
    }

    $uri     = sitefort_waf_server_value( 'REQUEST_URI' );
    $payload = json_encode(
        [
            'timestamp'   => time(),
            'ip'          => $ip,
            'reason'      => $reason,
            'http_status' => sitefort_waf_http_status_from_reason( $reason ),
            'request_uri' => $uri,
            'host'        => sitefort_waf_server_value( 'HTTP_HOST' ),
            'method'      => strtoupper( sitefort_waf_server_value( 'REQUEST_METHOD' ) ?: 'GET' ),
        ],
        JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE
    );
    if ( is_string( $payload ) && $payload !== '' ) {
        sitefort_waf_append_guarded_payload( $log_file, $payload . "\n" );
    }
}

function sitefort_waf_attack_type_from_reason( string $reason ): string {
    $reason = trim( $reason );
    if ( $reason === '' ) {
        return 'Firewall Block';
    }

    $map = [
        'Community blocklist' => 'Community Blocklist',
        'IP blocklist'        => 'Community Blocklist',
        'Rate limit'          => 'Rate Limit',
        'rate_limit'          => 'Rate Limit',
        'Country block'       => 'Country Block',
    ];

    $lower = strtolower( $reason );
    foreach ( $map as $prefix => $type ) {
        if ( str_starts_with( $lower, strtolower( $prefix ) ) ) {
            $suffix = substr( $reason, strlen( $prefix ) );
            return $type . $suffix;
        }
    }

    return ucfirst( $reason );
}

function sitefort_waf_http_status_from_reason( string $reason ): int {
    return str_starts_with( strtolower( trim( $reason ) ), 'rate limit' ) ? 429 : 403;
}

function sitefort_waf_guard_header(): string {
    return "<?php exit('Access denied'); __halt_compiler(); ?>\n";
}

function sitefort_waf_append_guarded_payload( string $path, string $payload ): bool {
    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Standalone bootstrap needs a lockable handle for guarded log appends.
    $handle = @fopen( $path, 'c+' );
    if ( false === $handle ) {
        return false;
    }

    $ok = false;

    if ( flock( $handle, LOCK_EX ) ) {
        if ( sitefort_waf_ensure_guarded_stream( $handle ) && 0 === fseek( $handle, 0, SEEK_END ) ) {
            $ok = sitefort_waf_write_all( $handle, $payload );
            fflush( $handle );
        }

        flock( $handle, LOCK_UN );
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing the bootstrap guarded log handle after append.
    fclose( $handle );

    return $ok;
}

/**
 * @param resource $handle
 */
function sitefort_waf_ensure_guarded_stream( $handle ): bool {
    $header = sitefort_waf_guard_header();

    if ( 0 !== fseek( $handle, 0 ) ) {
        return false;
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- Standalone bootstrap checks the guarded payload prefix before appending.
    $prefix = fread( $handle, strlen( $header ) );
    if ( is_string( $prefix ) && strncmp( $prefix, $header, strlen( $header ) ) === 0 ) {
        return true;
    }

    if ( 0 !== fseek( $handle, 0 ) ) {
        return false;
    }

    $existing = stream_get_contents( $handle );
    if ( false === $existing ) {
        $existing = '';
    }

    if ( ! ftruncate( $handle, 0 ) || 0 !== fseek( $handle, 0 ) ) {
        return false;
    }

    return sitefort_waf_write_all( $handle, $header . $existing );
}

/**
 * @param resource $handle
 */
function sitefort_waf_write_all( $handle, string $data ): bool {
    $written = 0;
    $length  = strlen( $data );

    while ( $written < $length ) {
        $result = fwrite( $handle, substr( $data, $written ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
        if ( false === $result || 0 === $result ) {
            return false;
        }

        $written += $result;
    }

    return true;
}

/**
 * Check whether an IP address falls within a given CIDR range.
 *
 * Supports both IPv4 and IPv6.
 *
 * @param string $ip   IP address to check.
 * @param string $cidr CIDR notation (e.g. "192.168.1.0/24" or "::1/128").
 * @return bool
 */
function sitefort_waf_ip_in_cidr( string $ip, string $cidr ): bool {
    $parts = explode( '/', $cidr, 2 );
    if ( count( $parts ) !== 2 ) {
        return false;
    }

    $subnet      = $parts[0];
    $prefix_len  = (int) $parts[1];

    $ip_bin     = @inet_pton( $ip );
    $subnet_bin = @inet_pton( $subnet );

    if ( $ip_bin === false || $subnet_bin === false ) {
        return false;
    }

    // Both addresses must be the same family (same byte length).
    if ( strlen( $ip_bin ) !== strlen( $subnet_bin ) ) {
        return false;
    }

    $byte_len  = strlen( $ip_bin ); // 4 for IPv4, 16 for IPv6
    $max_bits  = $byte_len * 8;

    if ( $prefix_len < 0 || $prefix_len > $max_bits ) {
        return false;
    }

    // Build the network mask.
    $mask = str_repeat( "\xff", (int) floor( $prefix_len / 8 ) );
    $remainder = $prefix_len % 8;
    if ( $remainder > 0 ) {
        $mask .= chr( 0xff << ( 8 - $remainder ) & 0xff );
    }
    $mask = str_pad( $mask, $byte_len, "\x00" );

    return ( $ip_bin & $mask ) === ( $subnet_bin & $mask );
}

/**
 * Check whether an IP matches a single entry (exact or CIDR).
 *
 * @param string $ip    IP to test.
 * @param string $entry Entry from the IP list (exact IP or CIDR).
 * @return bool
 */
function sitefort_waf_ip_matches( string $ip, string $entry ): bool {
    if ( strpos( $entry, '/' ) !== false ) {
        return sitefort_waf_ip_in_cidr( $ip, $entry );
    }
    // Binary comparison so different IPv6 representations match.
    $a = @inet_pton( $ip );
    $b = @inet_pton( $entry );
    return $a !== false && $b !== false && $a === $b;
}

/**
 * Determine whether the request should bypass firewall policy as a verified
 * search engine crawler.
 *
 * @param array  $config   WAF config array.
 * @param string $data_dir Data directory used for the verification cache.
 * @param string $ip       Client IP address.
 * @param string $engine   Verified-search engine identifier.
 * @return bool
 */
function sitefort_waf_is_verified_search_bot( array $config, string $data_dir, string $ip, string $engine ): bool {
    $use_mysqli_runtime_storage = function_exists( 'sitefort_waf_runtime_uses_mysqli' )
        && sitefort_waf_runtime_uses_mysqli( $config );

    return \SiteFort\Modules\Firewall\SearchTrust\SearchTrustDnsVerifier::verify(
        $ip,
        $engine,
        static function ( string $cache_key ) use ( $config, $data_dir, $use_mysqli_runtime_storage ): ?bool {
            if ( $use_mysqli_runtime_storage ) {
                if ( function_exists( 'sitefort_waf_db_search_trust_cache_get' ) ) {
                    return sitefort_waf_db_search_trust_cache_get( $config, $data_dir, $cache_key );
                }

                return null;
            }

            $cache_file = sitefort_waf_search_trust_cache_file( $data_dir, $cache_key );
            return sitefort_waf_search_trust_cache_get( $cache_file, $cache_key );
        },
        static function ( string $cache_key, bool $verified, int $ttl ) use ( $config, $data_dir, $use_mysqli_runtime_storage ): void {
            if ( $use_mysqli_runtime_storage ) {
                if ( function_exists( 'sitefort_waf_db_search_trust_cache_set' ) ) {
                    sitefort_waf_db_search_trust_cache_set( $config, $data_dir, $cache_key, $verified, $ttl );
                }

                return;
            }

            $cache_file = sitefort_waf_search_trust_cache_file( $data_dir, $cache_key );
            sitefort_waf_search_trust_cache_set( $cache_file, $cache_key, $verified, $ttl );
        }
    );
}

/**
 * Resolve the sharded search-trust cache file path for a cache key.
 *
 * @param string $data_dir
 * @param string $cache_key
 * @return string
 */
function sitefort_waf_search_trust_cache_file( string $data_dir, string $cache_key ): string {
    $cache_dir = $data_dir . '/waf-search-trust';
    if ( ! is_dir( $cache_dir ) ) {
        @mkdir( $cache_dir, 0755, true );
    }

    return $cache_dir . '/shard-' . \SiteFort\Modules\Firewall\SearchTrust\SearchTrustCore::get_verified_search_cache_shard( $cache_key ) . '.php';
}

/**
 * Read a cached search-trust decision.
 *
 * @param string $cache_file Cache file path.
 * @param string $cache_key  MD5 cache key for the claimed engine and client IP.
 * @return bool|null
 */
function sitefort_waf_search_trust_cache_get( string $cache_file, string $cache_key ): ?bool {
    $guard = "<?php exit; ?>\n";
    if ( ! is_file( $cache_file ) || ! is_readable( $cache_file ) ) {
        return null;
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Standalone bootstrap needs a lockable cache file handle before WordPress loads.
    $fp = @fopen( $cache_file, 'c+' );
    if ( $fp === false ) {
        return null;
    }

    if ( ! flock( $fp, LOCK_SH ) ) {
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing standalone bootstrap cache file handle after lock failure.
        fclose( $fp );
        return null;
    }

    $raw = stream_get_contents( $fp );
    flock( $fp, LOCK_UN );
    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing standalone bootstrap cache file handle after reading.
    fclose( $fp );

    if ( is_string( $raw ) && strncmp( $raw, $guard, strlen( $guard ) ) === 0 ) {
        $raw = substr( $raw, strlen( $guard ) );
    }

    $data = is_string( $raw ) ? @json_decode( $raw, true ) : null;
    if ( ! is_array( $data ) || ! isset( $data[ $cache_key ] ) || ! is_array( $data[ $cache_key ] ) ) {
        return null;
    }

    $entry = $data[ $cache_key ];
    if ( (int) ( $entry['expires_at'] ?? 0 ) <= time() ) {
        return null;
    }

    return ! empty( $entry['verified'] );
}

/**
 * Persist a search-trust decision with a bounded shard payload.
 *
 * @param string $cache_file Cache file path.
 * @param string $cache_key  MD5 cache key for the claimed engine and client IP.
 * @param bool   $verified   Verification result.
 * @param int    $ttl        Cache lifetime in seconds.
 * @return void
 */
function sitefort_waf_search_trust_cache_set( string $cache_file, string $cache_key, bool $verified, int $ttl ): void {
    $guard = "<?php exit; ?>\n";
    if ( ! is_dir( dirname( $cache_file ) ) ) {
        return;
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Standalone bootstrap needs a lockable cache file handle before WordPress loads.
    $fp = @fopen( $cache_file, 'c+' );
    if ( $fp === false ) {
        return;
    }

    if ( ! flock( $fp, LOCK_EX ) ) {
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing standalone bootstrap cache file handle after lock failure.
        fclose( $fp );
        return;
    }

    $raw = stream_get_contents( $fp );
    if ( is_string( $raw ) && strncmp( $raw, $guard, strlen( $guard ) ) === 0 ) {
        $raw = substr( $raw, strlen( $guard ) );
    }

    $data = is_string( $raw ) ? @json_decode( $raw, true ) : null;
    if ( ! is_array( $data ) ) {
        $data = [];
    }

    $now = time();
    $data = sitefort_waf_search_trust_cache_compact( $data, $now );

    $data[ $cache_key ] = [
        'verified'   => $verified,
        'expires_at' => $now + $ttl,
    ];

    $data = sitefort_waf_search_trust_cache_compact( $data, $now );

    ftruncate( $fp, 0 );
    rewind( $fp );
    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Standalone bootstrap persists its own search-trust cache before WordPress loads.
    fwrite( $fp, $guard . json_encode( $data ) );
    fflush( $fp );
    flock( $fp, LOCK_UN );
    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing standalone bootstrap cache file handle after writing.
    fclose( $fp );
}

/**
 * Remove expired/invalid entries and bound shard growth.
 *
 * @param array<string, mixed> $data
 * @param int                  $now
 * @return array<string, array{verified: bool, expires_at: int}>
 */
function sitefort_waf_search_trust_cache_compact( array $data, int $now ): array {
    $compacted = [];

    foreach ( $data as $key => $entry ) {
        if ( ! is_string( $key ) || $key === '' || ! is_array( $entry ) ) {
            continue;
        }

        $expires_at = (int) ( $entry['expires_at'] ?? 0 );
        if ( $expires_at <= $now ) {
            continue;
        }

        $compacted[ $key ] = [
            'verified'   => ! empty( $entry['verified'] ),
            'expires_at' => $expires_at,
        ];
    }

    if ( count( $compacted ) > 512 ) {
        uasort(
            $compacted,
            static function ( array $left, array $right ): int {
                return $right['expires_at'] <=> $left['expires_at'];
            }
        );
        $compacted = array_slice( $compacted, 0, 512, true );
    }

    return $compacted;
}

/**
 * Resolve the sharded rate-limit file path for an IP address.
 *
 * @param string $data_dir SiteFort data directory.
 * @param string $ip       Client IP address.
 * @return string
 */
function sitefort_waf_rate_limit_file( string $data_dir, string $ip ): string {
    $rate_dir = $data_dir . '/waf-rates';
    if ( ! is_dir( $rate_dir ) ) {
        @mkdir( $rate_dir, 0755, true );
    }

    $hash  = hash( 'crc32b', $ip );
    $shard = (int) ( hexdec( substr( $hash, -4 ) ) % 32 );
    return $rate_dir . '/shard-' . str_pad( (string) $shard, 2, '0', STR_PAD_LEFT ) . '.php';
}

$sitefort_waf_ip = sitefort_waf_get_ip( $sitefort_waf_config );

// Loopback requests (wp-cron, REST API health, scanner workers)
// must never be blocked by the WAF.
if ( in_array( $sitefort_waf_ip, [ '127.0.0.1', '::1' ], true ) ) {
    return;
}

$sitefort_waf_policy = is_array( $sitefort_waf_config['policy'] ?? null ) ? $sitefort_waf_config['policy'] : [];
$sitefort_waf_community_config = is_array( $sitefort_waf_config['community_blocklist'] ?? null ) ? $sitefort_waf_config['community_blocklist'] : [];
$sitefort_waf_search_scope = is_array( $sitefort_waf_config['search_exception'] ?? null ) ? $sitefort_waf_config['search_exception'] : [];
$sitefort_waf_search_scope_hash = trim( (string) ( $sitefort_waf_search_scope['scope_hash'] ?? '' ) );
$sitefort_waf_user_agent = sitefort_waf_server_value( 'HTTP_USER_AGENT' );
$sitefort_waf_search_engine = \SiteFort\Modules\Firewall\SearchTrust\SearchTrustCore::get_verified_search_engine_claim( $sitefort_waf_user_agent ) ?? '';
$sitefort_waf_verified_search_eligible = false;
$sitefort_waf_verified_search_checked = false;
$sitefort_waf_verified_search = false;

if (
    \SiteFort\Modules\Firewall\SearchTrust\SearchTrustCore::should_resolve_verified_search_exception(
        ! empty( $sitefort_waf_policy['rate_limit_enabled'] ),
        (string) ( $sitefort_waf_policy['country_mode'] ?? 'off' ) !== 'off' && ! empty( $sitefort_waf_policy['country_codes'] ),
        ! empty( $sitefort_waf_community_config['enabled'] )
    )
    && $sitefort_waf_search_scope_hash !== ''
    &&
    $sitefort_waf_search_engine !== ''
    && \SiteFort\Modules\Firewall\SearchTrust\SearchTrustCore::is_verified_search_request_eligible(
        [
            'method' => strtoupper( sitefort_waf_server_value( 'REQUEST_METHOD' ) ?: 'GET' ),
            'request_uri' => sitefort_waf_server_value( 'REQUEST_URI' ),
        ],
        $sitefort_waf_search_scope
    )
) {
    $sitefort_waf_verified_search_eligible = true;
    $sitefort_waf_verified_search_checked = true;
    $sitefort_waf_verified_search = sitefort_waf_is_verified_search_bot(
        $sitefort_waf_config,
        $sitefort_waf_data_dir,
        $sitefort_waf_ip,
        $sitefort_waf_search_engine
    );
}

if ( ! defined( \SiteFort\Modules\Firewall\SearchTrust\SearchTrustContext::BOOTSTRAP_CONSTANT ) ) {
    define(
        \SiteFort\Modules\Firewall\SearchTrust\SearchTrustContext::BOOTSTRAP_CONSTANT,
        \SiteFort\Modules\Firewall\SearchTrust\SearchTrustContext::create(
            [
                'checked' => $sitefort_waf_verified_search_checked,
                'eligible' => $sitefort_waf_verified_search_eligible,
                'verified' => $sitefort_waf_verified_search,
                'exception' => $sitefort_waf_verified_search,
                'engine' => $sitefort_waf_search_engine,
                'scope_hash' => $sitefort_waf_search_scope_hash,
            ]
        )
    );
}

$sitefort_waf_search_trust_context = \SiteFort\Modules\Firewall\SearchTrust\SearchTrustContext::from_bootstrap_constant();
$sitefort_waf_verified_search_exception = is_array( $sitefort_waf_search_trust_context ) && ! empty( $sitefort_waf_search_trust_context['exception'] );
$sitefort_waf_search_engine = is_array( $sitefort_waf_search_trust_context ) ? (string) ( $sitefort_waf_search_trust_context['engine'] ?? '' ) : '';

$sitefort_waf_country_code = $sitefort_waf_verified_search_exception ? '' : sitefort_waf_get_country_code( $sitefort_waf_config );
$sitefort_waf_country_policy_applied = (
    $sitefort_waf_verified_search_exception
    || (
    (string) ( $sitefort_waf_policy['country_mode'] ?? 'off' ) === 'off'
    || empty( $sitefort_waf_policy['country_codes'] )
    || $sitefort_waf_country_code !== ''
    )
);
$sitefort_waf_community_blocked = false;
$sitefort_waf_community_blocklist_applied = $sitefort_waf_verified_search_exception;
$sitefort_waf_community_enabled = ! empty( $sitefort_waf_community_config['enabled'] );
$sitefort_waf_expected_blocklist_signature = (string) ( $sitefort_waf_community_config['artifact_signature'] ?? '' );
$sitefort_waf_blocklist_file = $sitefort_waf_data_dir . '/waf-blocklist.php';
if (
    ! $sitefort_waf_verified_search_exception
    && $sitefort_waf_community_enabled
    && $sitefort_waf_expected_blocklist_signature !== ''
    && function_exists( 'sitefort_waf_db_blocklist_lookup' )
    && function_exists( 'sitefort_waf_runtime_uses_mysqli' )
    && sitefort_waf_runtime_uses_mysqli( $sitefort_waf_config )
) {
    $sitefort_waf_db_blocklist_result = sitefort_waf_db_blocklist_lookup(
        $sitefort_waf_config,
        $sitefort_waf_data_dir,
        $sitefort_waf_ip,
        $sitefort_waf_expected_blocklist_signature
    );

    if ( is_bool( $sitefort_waf_db_blocklist_result ) ) {
        $sitefort_waf_community_blocklist_applied = true;
        $sitefort_waf_community_blocked = $sitefort_waf_db_blocklist_result;
    }

    unset( $sitefort_waf_db_blocklist_result );
}

if (
    ! $sitefort_waf_verified_search_exception
    &&
    ! $sitefort_waf_community_blocklist_applied
    &&
    $sitefort_waf_community_enabled
    && $sitefort_waf_expected_blocklist_signature !== ''
    && is_file( $sitefort_waf_blocklist_file )
    && is_readable( $sitefort_waf_blocklist_file )
) {
    $sitefort_waf_blocklist = @include $sitefort_waf_blocklist_file;

    if (
        is_array( $sitefort_waf_blocklist )
        && hash_equals(
            $sitefort_waf_expected_blocklist_signature,
            (string) ( $sitefort_waf_blocklist['signature'] ?? '' )
        )
    ) {
        $sitefort_waf_community_blocklist_applied = true;

        if ( ! empty( $sitefort_waf_blocklist['ips'] )
            && isset( $sitefort_waf_blocklist['ips'][ $sitefort_waf_ip ] ) ) {
            $sitefort_waf_community_blocked = true;
        }

        if ( ! $sitefort_waf_community_blocked && ! empty( $sitefort_waf_blocklist['cidrs'] ) && is_array( $sitefort_waf_blocklist['cidrs'] ) ) {
            foreach ( $sitefort_waf_blocklist['cidrs'] as $sitefort_waf_bl_cidr ) {
                if ( sitefort_waf_ip_in_cidr( $sitefort_waf_ip, $sitefort_waf_bl_cidr ) ) {
                    $sitefort_waf_community_blocked = true;
                    break;
                }
            }
        }
    }

    unset( $sitefort_waf_blocklist, $sitefort_waf_bl_cidr );
}
unset( $sitefort_waf_blocklist_file );
unset( $sitefort_waf_community_config, $sitefort_waf_community_enabled, $sitefort_waf_expected_blocklist_signature );

if ( ! defined( 'SITEFORT_WAF_COMMUNITY_BLOCKLIST_APPLIED' ) ) {
    define( 'SITEFORT_WAF_COMMUNITY_BLOCKLIST_APPLIED', $sitefort_waf_community_blocklist_applied );
}

if ( ! defined( 'SITEFORT_WAF_COUNTRY_POLICY_APPLIED' ) ) {
    define( 'SITEFORT_WAF_COUNTRY_POLICY_APPLIED', $sitefort_waf_country_policy_applied );
}

$sitefort_waf_decision = \SiteFort\Modules\Firewall\RuleEngine::evaluate(
    $sitefort_waf_policy,
    [
        'ip' => $sitefort_waf_ip,
        'user_agent' => $sitefort_waf_user_agent,
        'community_blocked' => $sitefort_waf_community_blocked,
        'country_code' => $sitefort_waf_country_code,
        'country_lookup_performed' => $sitefort_waf_country_code !== '',
        'sensitive_path_match' => sitefort_waf_sensitive_path_matches( $sitefort_waf_policy ),
        'verified_search_exception' => $sitefort_waf_verified_search_exception,
        'verified_search_engine' => $sitefort_waf_verified_search_exception ? $sitefort_waf_search_engine : '',
    ]
);

if ( ( $sitefort_waf_decision['action'] ?? '' ) === 'allow_bypass' ) {
    return;
}

if ( ( $sitefort_waf_decision['action'] ?? '' ) === 'block' ) {
    $sitefort_waf_reason = (string) ( $sitefort_waf_decision['reason'] ?? 'Access denied' );
    $sitefort_waf_source = (string) ( $sitefort_waf_decision['source'] ?? '' );
    $sitefort_waf_message = 'This request was blocked due to suspicious activity.';
    if ( $sitefort_waf_source === 'blocked_ip' ) {
        $sitefort_waf_message = (string) ( $sitefort_waf_config['lockout_message'] ?? 'Access denied.' );
    } elseif ( $sitefort_waf_source === 'blocked_country' ) {
        $sitefort_waf_message = 'Access from your region is restricted by this site\'s security policy.';
    }

    sitefort_waf_log( $sitefort_waf_data_dir, $sitefort_waf_ip, $sitefort_waf_reason );
    sitefort_waf_block( $sitefort_waf_message, 403, $sitefort_waf_ip );
}

if ( ! empty( $sitefort_waf_policy['rate_limit_enabled'] ) ) {
    $sitefort_waf_rate_limit_path = sitefort_waf_request_uri_component(
        sitefort_waf_server_value( 'REQUEST_URI' ),
        'path'
    );

    if ( sitefort_waf_is_xmlrpc_path( $sitefort_waf_rate_limit_path ) ) {
        return;
    }

    if ( $sitefort_waf_verified_search_exception ) {
        return;
    }

    $sitefort_waf_rl_max            = (int) ( $sitefort_waf_policy['rate_limit_requests'] ?? 60 );
    if (
        function_exists( 'sitefort_waf_db_rate_limit_exceeded' )
        && function_exists( 'sitefort_waf_runtime_uses_mysqli' )
        && sitefort_waf_runtime_uses_mysqli( $sitefort_waf_config )
    ) {
        $sitefort_waf_db_rate_limit_exceeded = sitefort_waf_db_rate_limit_exceeded(
            $sitefort_waf_config,
            $sitefort_waf_data_dir,
            $sitefort_waf_ip,
            $sitefort_waf_rl_max
        );

        if ( $sitefort_waf_db_rate_limit_exceeded === true ) {
            sitefort_waf_log( $sitefort_waf_data_dir, $sitefort_waf_ip, 'Rate limit exceeded' );
            sitefort_waf_block( 'Request rate limit exceeded. Please wait before trying again.', 429, $sitefort_waf_ip );
        }

        if ( is_bool( $sitefort_waf_db_rate_limit_exceeded ) ) {
            return;
        }
    }

    $sitefort_waf_rl_file           = sitefort_waf_rate_limit_file( $sitefort_waf_data_dir, $sitefort_waf_ip );
    $sitefort_waf_now               = time();
    $sitefort_waf_current_bucket    = (int) floor( $sitefort_waf_now / 60 );
    $sitefort_waf_boundary_bucket   = $sitefort_waf_current_bucket - 1;

    // Guard prefix prevents Nginx from serving raw JSON with IP data.
    // On Nginx the .php extension triggers PHP-FPM which hits exit().
    $sitefort_waf_rl_guard                 = "<?php exit; ?>\n";
    $sitefort_waf_rl_max_file_size         = 2097152;
    $sitefort_waf_rl_max_entries_per_shard = 2048;

    if ( ! is_file( $sitefort_waf_rl_file ) ) {
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Standalone pre-WordPress bootstrap initializes its own rate-limit state file before WordPress is loaded.
        @file_put_contents( $sitefort_waf_rl_file, $sitefort_waf_rl_guard . json_encode( [] ), LOCK_EX );
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Standalone pre-WordPress bootstrap needs a lockable file handle for rate limiting before WordPress is loaded.
    $sitefort_waf_rl_fp = @fopen( $sitefort_waf_rl_file, 'c+' );
    if ( $sitefort_waf_rl_fp !== false ) {
        if ( flock( $sitefort_waf_rl_fp, LOCK_EX | LOCK_NB ) ) {
            $sitefort_waf_rl_stat = fstat( $sitefort_waf_rl_fp );
            if ( is_array( $sitefort_waf_rl_stat ) && (int) ( $sitefort_waf_rl_stat['size'] ?? 0 ) > $sitefort_waf_rl_max_file_size ) {
                $sitefort_waf_rl_data = [];
            } else {
                rewind( $sitefort_waf_rl_fp );
                $sitefort_waf_rl_raw = stream_get_contents( $sitefort_waf_rl_fp );
                if ( ! is_string( $sitefort_waf_rl_raw ) ) {
                    $sitefort_waf_rl_raw = '';
                }
                // Strip the PHP guard prefix before decoding JSON.
                if ( strncmp( $sitefort_waf_rl_raw, $sitefort_waf_rl_guard, strlen( $sitefort_waf_rl_guard ) ) === 0 ) {
                    $sitefort_waf_rl_raw = substr( $sitefort_waf_rl_raw, strlen( $sitefort_waf_rl_guard ) );
                }
                $sitefort_waf_rl_data = json_decode( $sitefort_waf_rl_raw, true );
            }

            if ( ! is_array( $sitefort_waf_rl_data ) ) {
                $sitefort_waf_rl_data = [];
            }

            foreach ( $sitefort_waf_rl_data as $sitefort_waf_rl_ip => &$sitefort_waf_rl_item ) {
                if ( ! is_array( $sitefort_waf_rl_item ) || ! isset( $sitefort_waf_rl_item['buckets'] ) || ! is_array( $sitefort_waf_rl_item['buckets'] ) ) {
                    unset( $sitefort_waf_rl_data[ $sitefort_waf_rl_ip ] );
                    continue;
                }

                foreach ( $sitefort_waf_rl_item['buckets'] as $sitefort_waf_rl_bucket => $sitefort_waf_rl_hits ) {
                    $sitefort_waf_rl_bucket = (int) $sitefort_waf_rl_bucket;
                    if ( $sitefort_waf_rl_bucket < $sitefort_waf_current_bucket - 1 ) {
                        unset( $sitefort_waf_rl_item['buckets'][ $sitefort_waf_rl_bucket ] );
                    }
                }

                if ( empty( $sitefort_waf_rl_item['buckets'] ) ) {
                    unset( $sitefort_waf_rl_data[ $sitefort_waf_rl_ip ] );
                }
            }
            unset( $sitefort_waf_rl_item );

            $sitefort_waf_rl_key = $sitefort_waf_ip;

            if ( ! isset( $sitefort_waf_rl_data[ $sitefort_waf_rl_key ] ) || ! is_array( $sitefort_waf_rl_data[ $sitefort_waf_rl_key ] ) ) {
                $sitefort_waf_rl_data[ $sitefort_waf_rl_key ] = [];
            }

            $sitefort_waf_rl_entry = &$sitefort_waf_rl_data[ $sitefort_waf_rl_key ];
            if ( ! isset( $sitefort_waf_rl_entry['buckets'] ) || ! is_array( $sitefort_waf_rl_entry['buckets'] ) ) {
                $sitefort_waf_rl_entry['buckets'] = [];
            }

            if ( ! isset( $sitefort_waf_rl_entry['buckets'][ $sitefort_waf_current_bucket ] ) ) {
                $sitefort_waf_rl_entry['buckets'][ $sitefort_waf_current_bucket ] = 0;
            }

            $sitefort_waf_rl_entry['buckets'][ $sitefort_waf_current_bucket ]++;
            $sitefort_waf_rl_entry['last_seen'] = $sitefort_waf_now;

            $sitefort_waf_rl_current_hits  = (int) ( $sitefort_waf_rl_entry['buckets'][ $sitefort_waf_current_bucket ] ?? 0 );
            $sitefort_waf_rl_boundary_hits = (int) ( $sitefort_waf_rl_entry['buckets'][ $sitefort_waf_boundary_bucket ] ?? 0 );
            $sitefort_waf_rl_seconds_in    = $sitefort_waf_now % 60;
            $sitefort_waf_rl_weighted_hits = $sitefort_waf_rl_current_hits
                + (int) floor( $sitefort_waf_rl_boundary_hits * ( 60 - $sitefort_waf_rl_seconds_in ) / 60 );
            $sitefort_waf_rl_exceeded = ( $sitefort_waf_rl_weighted_hits > $sitefort_waf_rl_max );
            unset( $sitefort_waf_rl_entry );

            if ( count( $sitefort_waf_rl_data ) > $sitefort_waf_rl_max_entries_per_shard ) {
                $sitefort_waf_rl_limited = [];
                if ( isset( $sitefort_waf_rl_data[ $sitefort_waf_rl_key ] ) ) {
                    $sitefort_waf_rl_limited[ $sitefort_waf_rl_key ] = $sitefort_waf_rl_data[ $sitefort_waf_rl_key ];
                }

                foreach ( $sitefort_waf_rl_data as $sitefort_waf_rl_ip => $sitefort_waf_rl_item ) {
                    if ( $sitefort_waf_rl_ip === $sitefort_waf_rl_key ) {
                        continue;
                    }
                    $sitefort_waf_rl_limited[ $sitefort_waf_rl_ip ] = $sitefort_waf_rl_item;
                    if ( count( $sitefort_waf_rl_limited ) >= $sitefort_waf_rl_max_entries_per_shard ) {
                        break;
                    }
                }

                $sitefort_waf_rl_data = $sitefort_waf_rl_limited;
                unset( $sitefort_waf_rl_limited, $sitefort_waf_rl_item );
            }

            // Write the updated data back (with PHP guard prefix).
            ftruncate( $sitefort_waf_rl_fp, 0 );
            rewind( $sitefort_waf_rl_fp );
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Standalone pre-WordPress bootstrap persists rate-limit state via its locked file handle.
            fwrite( $sitefort_waf_rl_fp, $sitefort_waf_rl_guard . json_encode( $sitefort_waf_rl_data ) );
            fflush( $sitefort_waf_rl_fp );
            flock( $sitefort_waf_rl_fp, LOCK_UN );
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Standalone pre-WordPress bootstrap closes its locked file handle after writing rate-limit state.
            fclose( $sitefort_waf_rl_fp );

            if ( $sitefort_waf_rl_exceeded ) {
                sitefort_waf_log( $sitefort_waf_data_dir, $sitefort_waf_ip, 'Rate limit exceeded' );
                sitefort_waf_block( 'Request rate limit exceeded. Please wait before trying again.', 429, $sitefort_waf_ip );
            }
        } else {
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Standalone pre-WordPress bootstrap closes its file handle when locking fails.
            fclose( $sitefort_waf_rl_fp );
        }
    }
}
