$maximum * 2 ) { return ''; } $value = sanitize_text_field( wp_unslash( $values[ $key ] ) ); return strlen( $value ) <= $maximum && $value === wp_unslash( $values[ $key ] ) ? $value : ''; } /** Preserve credential bytes; accept only the provider's bounded visible-ASCII token. */ private static function feedToken( array $values ): string { if ( ! isset( $values['feed_token'] ) || ! is_string( $values['feed_token'] ) || strlen( $values['feed_token'] ) > 1024 ) { return ''; } $raw = trim( wp_unslash( $values['feed_token'] ) ); $token = preg_replace( '/[^\x21-\x7e]/', '', $raw ); return is_string( $token ) && $token === $raw && strlen( $token ) >= 8 && strlen( $token ) <= 512 ? $token : ''; } private static function begin( string $operation ): void { echo '
'; wp_nonce_field( 'antivirus_ai_protection', '_avai_nonce' ); } public static function handle(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Administrator access is required.', 'shieldlume-security' ), '', array( 'response' => 403 ) ); } $method = isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Compare the already sanitized method with original routing bytes only to reject transformation; no original value is stored, displayed or used as an action. if ( 'POST' !== $method || $method !== ( $_SERVER['REQUEST_METHOD'] ?? null ) ) { wp_die( esc_html__( 'A POST request is required.', 'shieldlume-security' ), '', array( 'response' => 403 ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Only inspect nonce type and byte length before sanitation and the explicit verification immediately below; no action input or state is processed. if ( ! isset( $_POST['_avai_nonce'] ) || ! is_string( $_POST['_avai_nonce'] ) || strlen( $_POST['_avai_nonce'] ) > 40 ) { wp_die( 'Refresh this form and try again.', '', array( 'response' => 403 ) ); } $nonce = sanitize_text_field( wp_unslash( $_POST['_avai_nonce'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Compare the already sanitized nonce with its original unslashed bytes only to reject transformation; only the sanitized value reaches wp_verify_nonce. if ( '' === $nonce || strlen( $nonce ) > 20 || $nonce !== wp_unslash( $_POST['_avai_nonce'] ) ) { wp_die( 'Refresh this form and try again.', '', array( 'response' => 403 ) ); } if ( ! wp_verify_nonce( $nonce, 'antivirus_ai_protection' ) ) { wp_die( 'Refresh this form and try again.', '', array( 'response' => 403 ) ); } $operation = self::value( $_POST, 'operation', 32 ); if ( ! in_array( $operation, array( 'firewall', 'exception_add', 'exception_remove', 'feed_save', 'feed_remove', 'feed_refresh', 'rules_import', 'rules_enable', 'rules_disable', 'rules_refresh', 'notification_test' ), true ) ) { wp_die( 'Unknown operation.', '', array( 'response' => 400 ) ); } $store = new Store(); $token = null; $kind = 'success'; $message = ''; try { $token = $store->lock( 'protection_settings', 120 ); if ( ! $token ) { throw new \RuntimeException( 'Another protection change is in progress. Reload before retrying.' ); } Store::freshOption( 'antivirus_ai_firewall', array() ); $config = Firewall::configuration(); if ( 'firewall' === $operation ) { $mode = self::value( $_POST, 'mode' ); if ( ! in_array( $mode, array( 'off', 'observe', 'block' ), true ) ) { throw new \InvalidArgumentException( 'Choose a firewall mode.' ); } if ( 'block' === $mode && '1' !== self::value( $_POST, 'reviewed' ) ) { throw new \RuntimeException( 'Confirm that you tested your essential workflows in observation mode before enabling blocking.' ); } do_action( 'antivirus_ai_firewall_mode_changed', $mode ); $config['mode'] = $mode; $config['updated'] = time(); Store::saveOption( 'antivirus_ai_firewall', $config ); $message = 'Firewall preference saved. Review its current status and coverage below.'; } elseif ( 'exception_add' === $operation ) { $config['exceptions'] = array_values( array_filter( $config['exceptions'], static fn( $v ) => is_array( $v ) && (int) ( $v['expires'] ?? 0 ) > time() ) ); if ( count( $config['exceptions'] ) >= 32 ) { throw new \RuntimeException( 'Remove an existing exception before adding another (32 maximum).' ); } $input = array(); foreach ( array( 'rule', 'path', 'parameter', 'method' ) as $key ) { $input[ $key ] = self::value( $_POST, $key ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Inspect only input type and byte length before sanitize_text_field(wp_unslash()) in this same expression; the original reason is never stored or displayed. $input['reason'] = isset( $_POST['reason'] ) && is_string( $_POST['reason'] ) && strlen( $_POST['reason'] ) <= 600 ? sanitize_text_field( wp_unslash( $_POST['reason'] ) ) : ''; $config['exceptions'][] = Firewall::exception( $input ); do_action( 'antivirus_ai_firewall_exceptions_changed', $config['exceptions'] ); Store::saveOption( 'antivirus_ai_firewall', $config ); $message = 'This exact rule exception is active for seven days. Other rules remain in effect.'; } elseif ( 'exception_remove' === $operation ) { $id = self::value( $_POST, 'exception_id', 24 ); if ( ! preg_match( '/^[a-f0-9]{24}$/D', $id ) ) { throw new \InvalidArgumentException( 'Choose an existing exception.' ); } $config['exceptions'] = array_values( array_filter( $config['exceptions'], static fn( $v ) => is_array( $v ) && ( $v['id'] ?? '' ) !== $id ) ); do_action( 'antivirus_ai_firewall_exceptions_changed', $config['exceptions'] ); Store::saveOption( 'antivirus_ai_firewall', $config ); $message = 'The selected exception was removed.'; } elseif ( 'feed_save' === $operation ) { if ( '1' !== self::value( $_POST, 'feed_permission' ) ) { throw new \RuntimeException( 'Confirm permission to use your Wordfence feed access.' ); } if ( ! is_ssl() ) { throw new \RuntimeException( 'Use HTTPS to save a feed token.' ); } FeedConnection::save( self::feedToken( $_POST ) ); $message = 'Feed token encrypted and saved. Enable vulnerability checks in Protection settings, then request a refresh or a scan. No request was sent by saving.'; } elseif ( 'feed_remove' === $operation ) { FeedConnection::disconnect(); $message = 'Saved feed access and cached advisories removed; vulnerability requests disabled. A host-managed token, if present, remains on the server.'; } elseif ( 'feed_refresh' === $operation ) { if ( empty( Store::freshOption( 'antivirus_ai_settings', array() )['vulnerability_feed'] ) ) { throw new \RuntimeException( 'First enable vulnerability checks in Protection settings.' ); } $result = ( new Vulnerabilities( $store ) )->inspect( Inventory::collect() ); $message = 'Vulnerability check: ' . $result['status'] . '. Start a scan to record its findings with the current inventory.'; if ( ! empty( $result['limitations'] ) ) { $message .= ' ' . implode( ' ', array_slice( $result['limitations'], 0, 2 ) ); } } elseif ( 'notification_test' === $operation ) { Notifications::test(); $message = 'Test notification queued using your existing email permission. Use Resume scheduled work in Help & diagnostics if needed, then check your inbox and Activity. Transport acceptance does not prove delivery.'; } elseif ( 'rules_enable' === $operation ) { if ( '1' !== self::value( $_POST, 'rules_permission' ) ) { throw new \RuntimeException( 'Review the separate publisher disclosure and explicitly allow indicator downloads.' ); } RuleUpdates::setEnabled( true ); $message = 'Indicator downloads enabled for the configured publisher. The first scheduled check is pending; no sample or inventory is uploaded.'; } elseif ( 'rules_disable' === $operation ) { RuleUpdates::setEnabled( false ); $message = 'Future indicator downloads stopped. In-flight results will not be accepted. Existing verified indicators retain their expiry and revocation checks.'; } elseif ( 'rules_refresh' === $operation ) { $status = RuleUpdates::refresh(); $message = 'Indicator connection: ' . str_replace( '_', ' ', $status['status'] ) . '. Existing retry limits still apply.'; } elseif ( 'rules_import' === $operation ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This uploaded-file structure is not text. Require native upload provenance, integer size bounds and a verified signed JSON bundle before installation. $file = $_FILES['rule_bundle'] ?? null; if ( ! is_array( $file ) || UPLOAD_ERR_OK !== ( $file['error'] ?? -1 ) || ! is_string( $file['tmp_name'] ?? null ) || strlen( $file['tmp_name'] ) > 4096 || ! is_int( $file['size'] ?? null ) || ! is_uploaded_file( $file['tmp_name'] ) || (int) ( $file['size'] ?? 0 ) < 1 || (int) $file['size'] > RuleBundle::MAX_ENVELOPE_BYTES ) { throw new \RuntimeException( 'Choose a signed JSON indicator bundle of at most one MiB.' ); } $bytes = file_get_contents( $file['tmp_name'], false, null, 0, RuleBundle::MAX_ENVELOPE_BYTES + 1 ); if ( ! is_string( $bytes ) ) { throw new \RuntimeException( 'The uploaded bundle could not be read.' ); } $summary = RuleBundle::install( $bytes, $store ); $message = 'Signed indicator bundle verified and installed. Sequence ' . (int) $summary['sequence'] . '; ' . (int) $summary['indicator_count'] . ' indicators. Start a scan to use the new snapshot.'; } $store->activity( 'protection_setting_changed', array( 'operation' => $operation, 'actor' => get_current_user_id() ) ); } catch ( \Throwable $error ) { $kind = 'error'; $message = $error->getMessage(); } finally { if ( $token ) { $store->unlock( 'protection_settings', $token ); } } set_transient( 'antivirus_ai_notice_' . get_current_user_id(), array( 'kind' => $kind, 'message' => $message ), 120 ); wp_safe_redirect( Admin::url( 'protection' ) ); exit; } public static function overview(): void { $config = Firewall::configuration(); $settings = get_option( 'antivirus_ai_settings', array() ); $firewallDisabled = defined( 'ANTIVIRUS_AI_DISABLE_FIREWALL' ) && ANTIVIRUS_AI_DISABLE_FIREWALL; $loginDisabled = defined( 'ANTIVIRUS_AI_DISABLE_LOGIN_LIMIT' ) && true === ANTIVIRUS_AI_DISABLE_LOGIN_LIMIT; $firewallModes = array( 'off' => __( 'Off', 'shieldlume-security' ), 'observe' => __( 'Observe', 'shieldlume-security' ), 'block' => __( 'Block', 'shieldlume-security' ) ); $loginState = empty( $settings['login_limit'] ) ? __( 'Off', 'shieldlume-security' ) : ( $loginDisabled ? __( 'Suspended by host recovery switch', 'shieldlume-security' ) : ( LoginLimiter::conflicts() ? __( 'Suspended by overlapping protection', 'shieldlume-security' ) : __( 'Enabled', 'shieldlume-security' ) ) ); echo '
' . esc_html__( 'YOUR PROTECTION', 'shieldlume-security' ) . '

' . esc_html__( 'Know what is active.', 'shieldlume-security' ) . '

'; foreach ( array( __( 'Request firewall', 'shieldlume-security' ) => $firewallDisabled ? __( 'Suspended by host recovery switch', 'shieldlume-security' ) : ( $firewallModes[ $config['mode'] ] ?? __( 'Unavailable', 'shieldlume-security' ) ), __( 'Login throttling', 'shieldlume-security' ) => $loginState, __( 'Vulnerability feed', 'shieldlume-security' ) => empty( $settings['vulnerability_feed'] ) ? __( 'Off', 'shieldlume-security' ) : str_replace( '_', ' ', FeedConnection::status() ), __( 'Scheduled scans', 'shieldlume-security' ) => empty( $settings['daily'] ) ? __( 'Off', 'shieldlume-security' ) : __( 'Scheduled · check Help for timing', 'shieldlume-security' ) ) as $label => $state ) { echo '
' . esc_html( $label ) . '' . esc_html( $state ) . '
'; } echo '
' . esc_html__( 'Review protection', 'shieldlume-security' ) . '
'; } public static function render(): void { Experience::intro( __( 'PREVENT · CONNECT · REVIEW', 'shieldlume-security' ), __( 'Your protection, clearly configured.', 'shieldlume-security' ), __( 'Enable each protection deliberately. Review its coverage, test essential workflows and keep access to recovery.', 'shieldlume-security' ) ); echo ''; echo '

' . esc_html__( 'Protect your administrator account', 'shieldlume-security' ) . '

' . esc_html__( 'Use an authenticator or a passkey as a second step after your password, and keep one-use recovery codes offline. Review compatibility with integrations before enrolling.', 'shieldlume-security' ) . '

' . esc_html__( 'Open account protection', 'shieldlume-security' ) . '
'; $config = Firewall::configuration(); echo '

' . esc_html__( 'Request firewall', 'shieldlume-security' ) . '

' . esc_html__( 'A bounded local filter for six suspicious request patterns, plus separately configured source reputation, endpoint limits and one version-specific mitigation. It runs after WordPress loads; server-served files, earlier PHP execution, large or unsupported request bodies, and network DDoS attacks are outside its coverage. Begin in observation mode and test sign-in, forms, editing and checkout.', 'shieldlume-security' ) . '

'; $disabled = defined( 'ANTIVIRUS_AI_DISABLE_FIREWALL' ) && ANTIVIRUS_AI_DISABLE_FIREWALL; $modes = array( 'off' => __( 'Off', 'shieldlume-security' ), 'observe' => __( 'Observe', 'shieldlume-security' ), 'block' => __( 'Block', 'shieldlume-security' ) ); echo '

' . esc_html__( 'Current mode:', 'shieldlume-security' ) . ' ' . esc_html( $disabled ? __( 'Suspended by host recovery switch', 'shieldlume-security' ) : $modes[ $config['mode'] ] ) . '

'; $warning = get_option( 'antivirus_ai_firewall_warning', array() ); if ( ! empty( $warning['message'] ) ) { echo '

' . esc_html( $warning['message'] ) . ' ' . esc_html( gmdate( 'Y-m-d H:i', (int) $warning['at'] ) . ' UTC' ) . '

'; } self::begin( 'firewall' ); echo ''; submit_button( __( 'Save firewall mode', 'shieldlume-security' ), 'primary', 'submit', false ); echo '
' . esc_html__( 'Covered rules and emergency access', 'shieldlume-security' ) . '
    '; foreach ( Firewall::RULES as $id => $label ) { echo '
  • ' . esc_html( $id ) . ' — ' . esc_html( $label ) . '
  • '; } echo '

' . esc_html__( 'If a legitimate request is blocked, use your host’s file manager to set ANTIVIRUS_AI_DISABLE_FIREWALL to true in wp-config.php. Review the cause before restoring protection. Activity records are sampled and exclude request values and IP addresses.', 'shieldlume-security' ) . '

'; echo '
' . esc_html__( 'Temporary exceptions', 'shieldlume-security' ) . '

' . esc_html__( 'Exceptions apply to one rule, exact decoded path, method and parameter for seven days. Use query.name, form.name or json.name; nested fields use dots (for example json.address.city). Overlong or ambiguous field names cannot be excepted. A request matching another rule can still be blocked. Saved paths and reasons remain local; avoid personal data.', 'shieldlume-security' ) . '

'; foreach ( $config['exceptions'] as $exception ) { if ( ! is_array( $exception ) ) { continue; } echo '

' . esc_html( ( $exception['method'] ?? '' ) . ' ' . ( $exception['path'] ?? '' ) ) . ' · ' . esc_html( ( $exception['rule'] ?? '' ) . ' · ' . ( $exception['parameter'] ?? '' ) . ' · expires ' . gmdate( 'Y-m-d H:i', (int) ( $exception['expires'] ?? 0 ) ) . ' UTC' ) . '

'; self::begin( 'exception_remove' ); echo ''; submit_button( __( 'Remove exception', 'shieldlume-security' ), 'secondary', 'submit', false ); echo ''; } self::begin( 'exception_add' ); echo ''; foreach ( array( 'path' => array( 'Exact path', '/contact/', 200 ), 'parameter' => array( 'Parameter scope (or @path)', 'form.message', 128 ), 'reason' => array( 'Reason for this exception', '', 300 ) ) as $key => [$label, $placeholder, $max] ) { echo ''; } submit_button( __( 'Add seven-day exception', 'shieldlume-security' ), 'secondary', 'submit', false ); echo '
'; self::feed(); ThreatSettings::render(); ExposureChecks::render(); echo '

' . esc_html__( 'Check your alerts', 'shieldlume-security' ) . '

' . esc_html__( 'A test uses the recipient and independent email permission already saved in Settings. It includes no finding or source content. Check your inbox yourself; an accepted attempt only confirms the mail service accepted it. Requests are limited to one every five minutes.', 'shieldlume-security' ) . '

'; self::begin( 'notification_test' ); submit_button( __( 'Queue a test notification', 'shieldlume-security' ), 'secondary', 'submit', false ); echo '

' . esc_html__( 'Manage notification permission', 'shieldlume-security' ) . '

'; $rules = RuleBundle::summary(); echo '

' . esc_html__( 'Detection rules', 'shieldlume-security' ) . '

' . esc_html__( 'The bundled original structural checks update with the plugin. Additional signed SHA-256 indicators require an operator-trusted publisher and a reviewed bundle. A signature authenticates its publisher; detection quality depends on that publisher’s evidence and maintenance.', 'shieldlume-security' ) . '

' . esc_html__( 'Additional indicators:', 'shieldlume-security' ) . ' ' . esc_html( str_replace( '_', ' ', $rules['status'] ) ) . ' · ' . (int) $rules['indicator_count'] . '

' . esc_html( $rules['limitation'] ) . '

'; if ( RuleBundle::trustedKeys() ) { self::begin( 'rules_import' ); echo ''; submit_button( __( 'Verify and install bundle', 'shieldlume-security' ), 'secondary', 'submit', false ); echo ''; } else { echo '

' . esc_html__( 'No external indicator publisher is configured. Local structural checks remain available; no maintained malware hash corpus is included in this release.', 'shieldlume-security' ) . '

'; } self::ruleConnection(); echo '
'; do_action( 'antivirus_ai_protection_extensions' ); } private static function ruleConnection(): void { $state = RuleUpdates::status(); echo '

' . esc_html__( 'Automatic indicator updates', 'shieldlume-security' ) . '

' . esc_html__( 'Connection:', 'shieldlume-security' ) . ' ' . esc_html( str_replace( '_', ' ', $state['status'] ) ) . '

' . esc_html( $state['limitation'] ) . '

'; if ( $state['configured'] ) { echo '

' . esc_html__( 'Configured destination:', 'shieldlume-security' ) . ' ' . esc_html( $state['destination'] ) . ' · ' . esc_html__( 'Publisher privacy information', 'shieldlume-security' ) . '

'; if ( ! $state['enabled'] || 'consent_required' === $state['status'] ) { self::begin( 'rules_enable' ); echo ''; submit_button( __( 'Allow indicator downloads', 'shieldlume-security' ), 'primary', 'submit', false ); echo ''; } if ( $state['enabled'] ) { self::begin( 'rules_refresh' ); submit_button( __( 'Check indicator updates', 'shieldlume-security' ), 'secondary', 'submit', false ); echo ''; } } else { echo '

' . esc_html__( 'Your host must configure a reviewed publisher and trusted public keys before this connection can be enabled. A configured connection is not evidence of an effective or maintained malware corpus.', 'shieldlume-security' ) . '

'; } // Keep withdrawal available even if the publisher configuration becomes invalid. self::begin( 'rules_disable' ); submit_button( __( 'Stop indicator downloads', 'shieldlume-security' ), 'secondary', 'submit', false ); echo ''; if ( $state['last_success'] ) { echo '

' . esc_html__( 'Last verified download:', 'shieldlume-security' ) . ' ' . esc_html( $state['last_success'] ) . '

'; } if ( $state['missed_schedule'] ) { echo '

' . esc_html__( 'The update check is overdue. Review the host scheduler and connection; saved indicators may expire.', 'shieldlume-security' ) . '

'; } echo '
'; } private static function feed(): void { echo '

' . esc_html__( 'Vulnerability intelligence', 'shieldlume-security' ) . '

' . esc_html__( 'Connect your permitted Wordfence Intelligence token here. It is encrypted with this site’s WordPress authentication salts and never displayed again. Changing those salts requires reconnecting. On an authorized refresh, Wordfence receives the token and server network address; the full feed is downloaded and installed-component matching happens locally. No source code or customer records are sent.', 'shieldlume-security' ) . '

' . esc_html__( 'Connection:', 'shieldlume-security' ) . ' ' . esc_html( str_replace( '_', ' ', FeedConnection::status() ) ) . '

'; if ( is_string( getenv( 'ANTIVIRUS_AI_WORDFENCE_KEY' ) ) && '' !== getenv( 'ANTIVIRUS_AI_WORDFENCE_KEY' ) ) { echo '

' . esc_html__( 'Feed access is managed by your host. Its token takes precedence; ask the host to change it.', 'shieldlume-security' ) . '

'; } $cache = get_option( 'antivirus_ai_vulnerability_cache', array() ); if ( ! empty( $cache['fetched'] ) ) { echo '

' . esc_html__( 'Last successful feed download:', 'shieldlume-security' ) . ' ' . esc_html( gmdate( 'Y-m-d H:i', (int) $cache['fetched'] ) . ' UTC' ) . ' · ' . esc_html( time() - (int) $cache['fetched'] < 86400 ? 'within the daily cache window; inventory coverage is shown in scan results' : 'stale; request a refresh' ) . '

'; } echo '

' . esc_html__( 'Get feed access and review provider terms', 'shieldlume-security' ) . '

'; self::begin( 'feed_save' ); echo ''; submit_button( __( 'Save encrypted token', 'shieldlume-security' ), 'primary', 'submit', false ); echo ''; self::begin( 'feed_refresh' ); submit_button( __( 'Check permitted feed now', 'shieldlume-security' ), 'secondary', 'submit', false ); echo ''; self::begin( 'feed_remove' ); submit_button( __( 'Disconnect and remove saved feed data', 'shieldlume-security' ), 'secondary', 'submit', false ); echo '

' . esc_html__( 'Manage scan permissions and repository identities', 'shieldlume-security' ) . '

'; } }