16 ) { return false; } $method = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Compare the sanitized method with its original unslashed bytes only to reject rewriting; no raw value is stored, displayed or accepted as a method. return 'POST' === $method && $method === wp_unslash( $_SERVER['REQUEST_METHOD'] ); } public static function register(): void { WorkspaceDisplay::register(); add_action( 'admin_enqueue_scripts', array( self::class, 'assets' ), 30 ); add_filter( 'admin_body_class', array( self::class, 'bodyClass' ) ); add_action( 'admin_post_antivirus_ai_appearance', array( self::class, 'handle' ) ); add_action( 'wp_ajax_antivirus_ai_appearance', array( self::class, 'ajax' ) ); } /** Identifiers are sanitized and must already use their canonical spelling. */ private static function key( array $values, string $name ): string { if ( ! isset( $values[ $name ] ) || ! is_string( $values[ $name ] ) || strlen( $values[ $name ] ) > 128 ) { return ''; } $value = wp_unslash( $values[ $name ] ); $clean = sanitize_key( $value ); return strlen( $clean ) <= 64 && $clean === $value && $clean === $values[ $name ] ? $clean : ''; } private static function active(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only navigation uses allowlisted destinations; save() verifies actor, nonce and the exact preference allowlist before redirect. return current_user_can( 'manage_options' ) && in_array( self::key( $_GET, 'page' ), array( 'antivirus-ai', 'antivirus-ai-pro' ), true ); } public static function preference(): string { $value = get_user_meta( get_current_user_id(), self::META_KEY, true ); return is_string( $value ) && in_array( $value, self::CHOICES, true ) ? $value : 'light'; } public static function attributes(): string { return 'data-avai-theme="' . esc_attr( self::preference() ) . '"'; } public static function bodyClass( string $classes ): string { return self::active() ? $classes . ' avai-appearance-' . self::preference() . ' ' : $classes; } public static function assets(): void { if ( ! self::active() || ! wp_style_is( 'antivirus-ai-admin', 'enqueued' ) ) { return; } $dependencies = array( 'antivirus-ai-admin' ); if ( wp_style_is( 'antivirus-ai-pro-workspace', 'enqueued' ) ) { $dependencies[] = 'antivirus-ai-pro-workspace'; } $entry = ANTIVIRUS_AI_FILE; wp_enqueue_style( 'antivirus-ai-appearance', plugins_url( 'assets/appearance.css', $entry ), $dependencies, ANTIVIRUS_AI_VERSION ); wp_enqueue_script( 'antivirus-ai-appearance', plugins_url( 'assets/appearance.js', $entry ), array(), ANTIVIRUS_AI_VERSION, true ); } public static function toolbar(): void { $preference = self::preference(); echo '
'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only navigation uses allowlisted destinations; save() verifies actor, nonce and the exact preference allowlist before redirect. echo ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only local navigation; the appearance POST verifies its own nonce before saving and revalidates this context before redirecting. if ( 'antivirus-ai' === self::key( $_GET, 'page' ) && 'findings' === self::key( $_GET, 'tab' ) ) { Admin::findingContextFields(); } wp_nonce_field( 'antivirus_ai_appearance', '_avai_appearance_nonce', false ); echo '
'; } /** No caller can name another administrator or write a site preference. */ private static function save(): array { $actor = get_current_user_id(); if ( $actor < 1 || ! current_user_can( 'manage_options' ) ) { return array( 'status' => 403, 'message' => __( 'Administrator access is required.', 'shieldlume-security' ) ); } if ( ! self::postRequest() ) { return array( 'status' => 403, 'message' => __( 'A POST request is required.', 'shieldlume-security' ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- The original nonce is read only for type and byte-length guards; sanitize_text_field(wp_unslash()) runs here before the standalone verifier and any requested operation. $nonce = isset( $_POST['_avai_appearance_nonce'] ) && is_string( $_POST['_avai_appearance_nonce'] ) && strlen( $_POST['_avai_appearance_nonce'] ) <= 40 ? sanitize_text_field( wp_unslash( $_POST['_avai_appearance_nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Original nonce bytes are used only to reject sanitizer changes; only the bounded sanitized nonce reaches the following standalone verifier. if ( '' === $nonce || strlen( $nonce ) > 20 || $nonce !== wp_unslash( $_POST['_avai_appearance_nonce'] ) ) { return array( 'status' => 403, 'message' => __( 'The appearance request expired. Refresh this page and try again.', 'shieldlume-security' ) ); } if ( ! wp_verify_nonce( $nonce, 'antivirus_ai_appearance' ) ) { return array( 'status' => 403, 'message' => __( 'The appearance request expired. Refresh this page and try again.', 'shieldlume-security' ) ); } $preference = self::key( $_POST, 'appearance' ); if ( ! in_array( $preference, self::CHOICES, true ) ) { return array( 'status' => 400, 'message' => __( 'Choose Light, Dark or System.', 'shieldlume-security' ) ); } if ( false === update_user_meta( $actor, self::META_KEY, $preference ) && get_user_meta( $actor, self::META_KEY, true ) !== $preference ) { return array( 'status' => 500, 'message' => __( 'Your appearance preference could not be saved. Please try again.', 'shieldlume-security' ) ); } return array( 'status' => 200, 'redirect' => self::redirectUrl( $_POST ), 'appearance' => $preference, 'message' => __( 'Appearance saved for your administrator account.', 'shieldlume-security' ) ); } private static function redirectUrl( array $values ): string { if ( 'antivirus-ai-pro' === self::key( $values, 'return_page' ) && class_exists( '\AntivirusAI\Pro\Workspace' ) ) { return \AntivirusAI\Pro\Workspace::url( self::key( $values, 'return_view' ) ); } $tab = self::key( $values, 'return_tab' ); return Admin::url( $tab, 'findings' === $tab ? Admin::findingContext( $values, 'return_' ) : array() ); } public static function handle(): void { $result = self::save(); if ( 200 !== $result['status'] ) { wp_die( esc_html( $result['message'] ), '', array( 'response' => (int) $result['status'] ) ); return; } wp_safe_redirect( $result['redirect'] ); exit; } public static function ajax(): void { $result = self::save(); if ( 200 !== $result['status'] ) { wp_send_json_error( array( 'message' => $result['message'] ), $result['status'] ); return; } wp_send_json_success( array( 'appearance' => $result['appearance'], 'message' => $result['message'] ) ); } }