settings = $settings; $this->database = $database; $this->activity_log = $activity_log; $this->setup_tabs(); $this->init_hooks(); } /** * Setup available tabs */ private function setup_tabs() { $this->tabs = array( 'dashboard' => __( 'Dashboard', 'vigilante' ), 'firewall' => __( 'Firewall', 'vigilante' ), 'headers' => __( 'Security Headers', 'vigilante' ), 'login' => __( 'Login Security', 'vigilante' ), 'rest-api' => __( 'REST API', 'vigilante' ), 'users' => __( 'User Security', 'vigilante' ), 'wp-hardening' => __( 'WP Hardening', 'vigilante' ), 'file-integrity' => __( 'File Integrity', 'vigilante' ), 'activity-log' => __( 'Security Audit', 'vigilante' ), 'tools' => __( 'Settings & Tools', 'vigilante' ), ); } /** * Initialize hooks */ private function init_hooks() { add_action( 'admin_menu', array( $this, 'add_menu' ) ); add_action( 'admin_init', array( $this, 'redirect_submenu_shortcuts' ) ); add_action( 'admin_init', array( $this, 'register_settings' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'admin_notices', array( $this, 'show_admin_notices' ) ); // Highlight correct submenu based on active tab add_filter( 'submenu_file', array( $this, 'highlight_submenu_tab' ) ); // Set browser tab title to show plugin name and active tab add_filter( 'admin_title', array( $this, 'set_admin_page_title' ), 10, 2 ); // AJAX handlers add_action( 'wp_ajax_vigilante_save_settings', array( $this, 'ajax_save_settings' ) ); add_action( 'wp_ajax_vigilante_apply_preset', array( $this, 'ajax_apply_preset' ) ); add_action( 'wp_ajax_vigilante_reset_section', array( $this, 'ajax_reset_section' ) ); add_action( 'wp_ajax_vigilante_clear_lockouts', array( $this, 'ajax_clear_lockouts' ) ); add_action( 'wp_ajax_vigilante_clear_logs', array( $this, 'ajax_clear_logs' ) ); add_action( 'wp_ajax_vigilante_run_scan', array( $this, 'ajax_run_scan' ) ); add_action( 'wp_ajax_vigilante_clear_scan', array( $this, 'ajax_clear_scan' ) ); add_action( 'wp_ajax_vigilante_ignore_file', array( $this, 'ajax_ignore_file' ) ); add_action( 'wp_ajax_vigilante_unignore_file', array( $this, 'ajax_unignore_file' ) ); add_action( 'wp_ajax_vigilante_bulk_ignore_files', array( $this, 'ajax_bulk_ignore_files' ) ); add_action( 'wp_ajax_vigilante_bulk_unignore_files', array( $this, 'ajax_bulk_unignore_files' ) ); add_action( 'wp_ajax_vigilante_clear_ignored', array( $this, 'ajax_clear_ignored' ) ); add_action( 'wp_ajax_vigilante_ignore_closed_plugin', array( $this, 'ajax_ignore_closed_plugin' ) ); add_action( 'wp_ajax_vigilante_unignore_closed_plugin', array( $this, 'ajax_unignore_closed_plugin' ) ); add_action( 'wp_ajax_vigilante_clear_ignored_closed_plugins', array( $this, 'ajax_clear_ignored_closed_plugins' ) ); add_action( 'wp_ajax_vigilante_approve_critical_file', array( $this, 'ajax_approve_critical_file' ) ); add_action( 'wp_ajax_vigilante_export_settings', array( $this, 'ajax_export_settings' ) ); add_action( 'wp_ajax_vigilante_import_settings', array( $this, 'ajax_import_settings' ) ); add_action( 'wp_ajax_vigilante_get_logs', array( $this, 'ajax_get_logs' ) ); add_action( 'wp_ajax_vigilante_test_headers', array( $this, 'ajax_test_headers' ) ); add_action( 'wp_ajax_vigilante_download_files_backup', array( $this, 'ajax_download_files_backup' ) ); // 2FA AJAX handlers add_action( 'wp_ajax_vigilante_search_users_2fa', array( $this, 'ajax_search_users_2fa' ) ); add_action( 'wp_ajax_vigilante_send_2fa_notification', array( $this, 'ajax_send_2fa_notification' ) ); add_action( 'wp_ajax_vigilante_search_totp_users', array( $this, 'ajax_search_totp_users' ) ); add_action( 'wp_ajax_vigilante_reset_totp_users', array( $this, 'ajax_reset_totp_users' ) ); add_action( 'wp_ajax_vigilante_totp_get_setup', array( $this, 'ajax_totp_get_setup' ) ); add_action( 'wp_ajax_vigilante_notify_login_url', array( $this, 'ajax_notify_login_url' ) ); // Password Reset AJAX handlers add_action( 'wp_ajax_vigilante_search_users_password_reset', array( $this, 'ajax_search_users_password_reset' ) ); add_action( 'wp_ajax_vigilante_force_password_reset', array( $this, 'ajax_force_password_reset' ) ); add_action( 'wp_ajax_vigilante_force_password_reset_all', array( $this, 'ajax_force_password_reset_all' ) ); add_action( 'wp_ajax_vigilante_force_password_reset_by_role', array( $this, 'ajax_force_password_reset_by_role' ) ); // User approval AJAX handlers add_action( 'wp_ajax_vigilante_approve_user', array( $this, 'ajax_approve_user' ) ); add_action( 'wp_ajax_vigilante_reject_user', array( $this, 'ajax_reject_user' ) ); // Session management AJAX handlers add_action( 'wp_ajax_vigilante_get_user_sessions', array( $this, 'ajax_get_user_sessions' ) ); add_action( 'wp_ajax_vigilante_revoke_session', array( $this, 'ajax_revoke_session' ) ); add_action( 'wp_ajax_vigilante_revoke_all_sessions', array( $this, 'ajax_revoke_all_sessions' ) ); // Under Attack mode AJAX handlers add_action( 'wp_ajax_vigilante_activate_under_attack', array( $this, 'ajax_activate_under_attack' ) ); add_action( 'wp_ajax_vigilante_deactivate_under_attack', array( $this, 'ajax_deactivate_under_attack' ) ); add_action( 'wp_ajax_vigilante_under_attack_status', array( $this, 'ajax_under_attack_status' ) ); // Database backup AJAX handlers add_action( 'wp_ajax_vigilante_get_db_tables', array( $this, 'ajax_get_db_tables' ) ); add_action( 'wp_ajax_vigilante_download_db_backup', array( $this, 'ajax_download_db_backup' ) ); // Database prefix AJAX handlers add_action( 'wp_ajax_vigilante_generate_prefix', array( $this, 'ajax_generate_prefix' ) ); add_action( 'wp_ajax_vigilante_change_prefix', array( $this, 'ajax_change_prefix' ) ); // Firewall list management from activity log popup add_action( 'wp_ajax_vigilante_add_to_firewall_list', array( $this, 'ajax_add_to_firewall_list' ) ); add_action( 'wp_ajax_vigilante_unblock_firewall_ip', array( $this, 'ajax_unblock_firewall_ip' ) ); // Security Analyzer AJAX handlers (v2.1.0) add_action( 'wp_ajax_vigilante_analyzer_run', array( $this, 'ajax_analyzer_run' ) ); add_action( 'wp_ajax_vigilante_analyzer_history', array( $this, 'ajax_analyzer_history' ) ); add_action( 'wp_ajax_vigilante_analyzer_dismiss_notice', array( $this, 'ajax_analyzer_dismiss_notice' ) ); add_action( 'wp_ajax_vigilante_analyzer_save_settings', array( $this, 'ajax_analyzer_save_settings' ) ); // Shared "Send test email" handler — Notification settings, File Integrity, Audit Alerts (v2.8.0) add_action( 'wp_ajax_vigilante_send_test_email', array( $this, 'ajax_send_test_email' ) ); // Run migrations on admin load add_action( 'admin_init', array( $this, 'run_migrations' ) ); } /** * Run database migrations based on stored version */ public function run_migrations() { $db_version = get_option( 'vigilante_db_version', '0' ); // 1.2.3: Fix IP lists corrupted by sanitize_text_field stripping newlines if ( version_compare( $db_version, '1.2.3', '<' ) ) { $this->migrate_fix_ip_lists(); update_option( 'vigilante_db_version', '1.2.3' ); } // 1.3.0: Add request_method column to activity log table if ( version_compare( $db_version, '1.3.0', '<' ) ) { $this->database->run_migrations(); } // 1.9.0: Re-apply wp-config constants (performance constants removed from managed list) if ( version_compare( $db_version, '1.9.0', '<' ) ) { if ( $this->settings->is_module_enabled( 'wp_hardening' ) ) { require_once VIGILANTE_INCLUDES_DIR . 'class-wpconfig-security.php'; $wpconfig = new Vigilante_Wpconfig_Security( $this->settings ); $wpconfig->apply_security_constants(); } update_option( 'vigilante_db_version', '1.9.0' ); } // 1.10.0: Clean up orphaned email fields (centralized notification recipients) if ( version_compare( $db_version, '1.10.0', '<' ) ) { $this->migrate_cleanup_email_fields(); update_option( 'vigilante_db_version', '1.10.0' ); } // 1.11.0: Remove stale 'enabled' key from activity_log settings // + Convert additional_recipients from string to array if ( version_compare( $db_version, '1.11.0', '<' ) ) { $options = get_option( Vigilante_Settings::OPTION_NAME, array() ); $changed = false; if ( isset( $options['activity_log']['enabled'] ) ) { unset( $options['activity_log']['enabled'] ); $changed = true; } // Convert corrupted string to array for additional_recipients if ( isset( $options['email']['additional_recipients'] ) && is_string( $options['email']['additional_recipients'] ) ) { $raw = trim( $options['email']['additional_recipients'] ); if ( ! empty( $raw ) ) { $emails = array_filter( array_map( 'trim', preg_split( '/[\r\n,; ]+/', $raw ) ) ); $options['email']['additional_recipients'] = array_values( array_filter( $emails, 'is_email' ) ); } else { $options['email']['additional_recipients'] = array(); } $changed = true; } if ( $changed ) { update_option( Vigilante_Settings::OPTION_NAME, $options ); } update_option( 'vigilante_db_version', '1.11.0' ); } // 1.12.1: Regenerate htaccess (WooCommerce IPN exclusion in bot blocking rule) if ( version_compare( $db_version, '1.12.1', '<' ) ) { if ( ! empty( $this->settings->get_section( 'firewall' )['block_bad_bots'] ) ) { require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php'; $htaccess = new Vigilante_Htaccess_Protection( $this->settings ); $htaccess->apply_rules(); } update_option( 'vigilante_db_version', '1.12.1' ); } // 1.14.0: Generate critical config files baseline (wp-config.php, .htaccess) if ( version_compare( $db_version, '1.14.0', '<' ) ) { if ( ! class_exists( 'Vigilante_File_Integrity' ) ) { require_once VIGILANTE_INCLUDES_DIR . 'class-file-integrity.php'; } $fi = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log ); $fi->regenerate_all_baselines(); update_option( 'vigilante_db_version', '1.14.0' ); } // 2.0.0: Move hide_server_signature and remove_fingerprinting_headers // from firewall section to security_headers section if ( version_compare( $db_version, '2.0.0', '<' ) ) { $options = get_option( Vigilante_Settings::OPTION_NAME, array() ); $changed = false; foreach ( array( 'hide_server_signature', 'remove_fingerprinting_headers' ) as $key ) { if ( isset( $options['firewall'][ $key ] ) ) { if ( ! isset( $options['security_headers'][ $key ] ) ) { $options['security_headers'][ $key ] = $options['firewall'][ $key ]; } unset( $options['firewall'][ $key ] ); $changed = true; } } if ( $changed ) { update_option( Vigilante_Settings::OPTION_NAME, $options ); } update_option( 'vigilante_db_version', '2.0.0' ); } // 2.6.1: Two things happen here. // // 1. Re-apply wp-config constants so the block is rewritten with // "if ( ! defined() )" guards around every define(). Without guards, // non-standard setups that pre-define WordPress constants outside // wp-config.php (custom bootstraps that load constants from .env or // similar) hit a fatal "Constant already defined" when wp-config.php // is parsed and reaches our block. // // 2. Drop the cached Security Check report. The cached "max" per // category was frozen at scan time; with the internal category // bumping from 22 to 28 points (closed_plugins added in 2.6.0), // the cached report would keep displaying 22/22 until the next // full scan. Clearing it forces a fresh scan with the new caps. if ( version_compare( $db_version, '2.6.1', '<' ) ) { if ( $this->settings->is_module_enabled( 'wp_hardening' ) ) { require_once VIGILANTE_INCLUDES_DIR . 'class-wpconfig-security.php'; $wpconfig = new Vigilante_Wpconfig_Security( $this->settings ); $wpconfig->apply_security_constants(); } delete_option( 'vigilante_analyzer_last_scan' ); // Schedule an immediate background scan so the dashboard widget // doesn't display "Last scan: never" right after the upgrade. // Reuses the same hook the post-Under-Attack flow uses. if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) { wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' ); } update_option( 'vigilante_db_version', '2.6.1' ); } // 2.9.3: Regenerate the .htaccess protection block. The bad-bots // User-Agent list dropped substring-prone tokens that 403'd // legitimate clients (e.g. "rma" matched inside "Performance" and // blocked WP Rocket's page fetch), and the blocking rules now honour // the firewall IP / User-Agent whitelists as negated exceptions. // Existing sites only rewrite the block when Server Protection is // saved, so the upgrade has to refresh it once itself (same pattern // as the 1.12.1 WooCommerce IPN migration). if ( version_compare( $db_version, '2.9.3', '<' ) ) { require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php'; $htaccess = new Vigilante_Htaccess_Protection( $this->settings ); if ( $htaccess->are_rules_active() ) { $htaccess->apply_rules(); } update_option( 'vigilante_db_version', '2.9.3' ); } } /** * Migration: Remove orphaned email fields from saved options * * v1.10.0 centralized notification recipients into email section. * Old per-module notify_email fields and dead email section fields * are removed to avoid confusion. */ private function migrate_cleanup_email_fields() { $options = get_option( Vigilante_Settings::OPTION_NAME, array() ); $modified = false; // Remove orphaned fields from email section $dead_email_keys = array( 'enabled', 'from_name', 'from_email', 'admin_email', 'send_activation_email', 'custom_email' ); if ( isset( $options['email'] ) && is_array( $options['email'] ) ) { foreach ( $dead_email_keys as $key ) { if ( array_key_exists( $key, $options['email'] ) ) { unset( $options['email'][ $key ] ); $modified = true; } } // Ensure new fields exist with defaults if ( ! array_key_exists( 'send_to_admin_email', $options['email'] ) ) { $options['email']['send_to_admin_email'] = true; $modified = true; } if ( ! array_key_exists( 'additional_recipients', $options['email'] ) ) { $options['email']['additional_recipients'] = ''; $modified = true; } } // Remove notify_email from login_security if ( isset( $options['login_security']['notify_email'] ) ) { unset( $options['login_security']['notify_email'] ); $modified = true; } // Remove notify_email from file_integrity if ( isset( $options['file_integrity']['notify_email'] ) ) { unset( $options['file_integrity']['notify_email'] ); $modified = true; } if ( $modified ) { update_option( Vigilante_Settings::OPTION_NAME, $options ); // Clear settings cache so the plugin uses clean data immediately $this->settings->clear_cache(); } } /** * Migration: Fix IP whitelist/blacklist entries merged into single line * * Prior to 1.2.3, sanitize_text_field() stripped newlines from textarea data, * causing multiple IPs to be stored as a single space-separated string. */ private function migrate_fix_ip_lists() { $options = get_option( Vigilante_Settings::OPTION_NAME, array() ); $fixed = false; foreach ( array( 'ip_whitelist', 'ip_blacklist' ) as $key ) { if ( ! empty( $options['firewall'][ $key ] ) && is_array( $options['firewall'][ $key ] ) ) { $new_list = array(); foreach ( $options['firewall'][ $key ] as $entry ) { // Split entries that were joined by spaces $parts = preg_split( '/\s+/', trim( $entry ) ); foreach ( $parts as $part ) { $part = trim( $part ); if ( '' !== $part ) { $new_list[] = $part; } } } if ( count( $new_list ) !== count( $options['firewall'][ $key ] ) ) { $options['firewall'][ $key ] = array_unique( $new_list ); $fixed = true; } } } if ( $fixed ) { update_option( Vigilante_Settings::OPTION_NAME, $options ); // Clear settings cache so changes take effect immediately $this->settings->clear_cache(); } } /** * Add admin menu page in last position */ public function add_menu() { $menu_title = __( 'Vigilant', 'vigilante' ); // Count pending approvals (separate concern, always red if present) $pending_count = $this->get_pending_approvals_count(); // Get security issues with severity $security_status = $this->get_security_status_for_badge(); // Total count for badge $total_badge = $pending_count + $security_status['count']; if ( $total_badge > 0 ) { // Determine badge color: // - Red (awaiting-mod): pending approvals OR critical modules disabled // - Orange (update-plugins): only non-critical modules disabled if ( $pending_count > 0 || $security_status['has_critical'] ) { $badge_class = 'awaiting-mod'; } else { $badge_class = 'update-plugins vigilante-badge-warning'; } $menu_title .= sprintf( ' %d', esc_attr( $badge_class ), $total_badge, $total_badge ); } add_menu_page( __( 'Vigilant', 'vigilante' ), $menu_title, 'manage_options', 'vigilante', array( $this, 'render_settings_page' ), 'dashicons-shield', 999 ); // Rename auto-generated first submenu to "Dashboard" add_submenu_page( 'vigilante', __( 'Dashboard', 'vigilante' ), __( 'Dashboard', 'vigilante' ), 'manage_options', 'vigilante', array( $this, 'render_settings_page' ) ); // Security Audit shortcut add_submenu_page( 'vigilante', __( 'Security Audit', 'vigilante' ), __( 'Security Audit', 'vigilante' ), 'manage_options', 'vigilante-activity-log', array( $this, 'redirect_to_tab' ) ); // File Integrity shortcut add_submenu_page( 'vigilante', __( 'File Integrity', 'vigilante' ), __( 'File Integrity', 'vigilante' ), 'manage_options', 'vigilante-file-integrity', array( $this, 'redirect_to_tab' ) ); } /** * Redirect submenu shortcuts early, before headers are sent */ public function redirect_submenu_shortcuts() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Just reading page slug for redirect $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; $tab_map = array( 'vigilante-activity-log' => 'activity-log', 'vigilante-file-integrity' => 'file-integrity', ); if ( isset( $tab_map[ $page ] ) ) { wp_safe_redirect( admin_url( 'admin.php?page=vigilante&tab=' . $tab_map[ $page ] ) ); exit; } } /** * Fallback redirect for submenu shortcuts (JS-based) */ public function redirect_to_tab() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Just reading page slug for redirect $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; $tab_map = array( 'vigilante-activity-log' => 'activity-log', 'vigilante-file-integrity' => 'file-integrity', ); if ( isset( $tab_map[ $page ] ) ) { $url = admin_url( 'admin.php?page=vigilante&tab=' . $tab_map[ $page ] ); echo ''; } } /** * Highlight the correct submenu item based on active tab * * @param string $submenu_file Current submenu file. * @return string */ public function highlight_submenu_tab( $submenu_file ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading tab for menu highlight only $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : ''; if ( 'vigilante' !== $page || empty( $tab ) ) { return $submenu_file; } $tab_to_submenu = array( 'activity-log' => 'vigilante-activity-log', 'file-integrity' => 'vigilante-file-integrity', ); if ( isset( $tab_to_submenu[ $tab ] ) ) { return $tab_to_submenu[ $tab ]; } return $submenu_file; } /** * Set browser tab title to show plugin name and active tab * * Changes "Dashboard ‹ Site Name — WordPress" to * "Vigilant > Dashboard ‹ Site Name — WordPress" * * @param string $admin_title Full admin title. * @param string $title Page title from add_menu_page/add_submenu_page. * @return string Modified title. */ public function set_admin_page_title( $admin_title, $title ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading page slug for title only $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; // Only modify on Vigilante pages if ( 0 !== strpos( $page, 'vigilante' ) ) { return $admin_title; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'dashboard'; if ( isset( $this->tabs[ $tab ] ) ) { $tab_label = $this->tabs[ $tab ]; } else { $tab_label = __( 'Dashboard', 'vigilante' ); } $plugin_title = __( 'Vigilant', 'vigilante' ) . ' › ' . $tab_label; // Replace the original page title portion return str_replace( $title, $plugin_title, $admin_title ); } /** * Get count of users pending approval * * @return int Count of pending users. */ private function get_pending_approvals_count() { // Prevent early execution before WordPress is ready if ( ! did_action( 'plugins_loaded' ) ) { return 0; } $registration_approval = $this->settings->get_section( 'user_security' ); $approval_settings = $registration_approval['registration_approval'] ?? array(); if ( empty( $approval_settings['enabled'] ) ) { return 0; } // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Limited results in admin context. $pending_users = get_users( array( 'meta_key' => 'vigilante_pending_approval', 'meta_value' => '1', 'fields' => 'ID', ) ); // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value return count( $pending_users ); } /** * Calculate comprehensive security score (0-100) * * @param array $options Plugin options. * @return int Security score. */ private function calculate_security_score( $options ) { $score = 0; $max_score = 0; // Module scores (60 points total) $module_weights = array( 'firewall' => 10, 'security_headers' => 8, 'login_security' => 10, 'rest_api_security' => 6, 'user_security' => 8, 'wp_hardening' => 8, 'file_integrity' => 5, 'activity_log' => 5, ); foreach ( $module_weights as $module => $weight ) { $max_score += $weight; if ( ! empty( $options['modules'][ $module ] ) ) { $score += $weight; } } // Firewall details (10 points) if ( ! empty( $options['modules']['firewall'] ) ) { $firewall = $options['firewall'] ?? array(); $max_score += 10; $firewall_checks = array( 'block_sql_injection', 'block_xss_attacks', 'block_bad_query_strings', 'block_file_inclusion', 'block_directory_traversal', ); $firewall_enabled = 0; foreach ( $firewall_checks as $check ) { if ( ! empty( $firewall[ $check ] ) ) { $firewall_enabled++; } } $score += min( 10, $firewall_enabled * 2 ); } // Login security details (10 points) if ( ! empty( $options['modules']['login_security'] ) ) { $login = $options['login_security'] ?? array(); $max_score += 10; // Max attempts configured if ( isset( $login['max_attempts'] ) && $login['max_attempts'] <= 5 ) { $score += 3; } // XML-RPC disabled if ( ! empty( $login['disable_xmlrpc'] ) ) { $score += 3; } // 2FA enabled if ( ! empty( $login['two_factor']['enabled'] ) ) { $score += 4; } } // Security headers details (10 points) if ( ! empty( $options['modules']['security_headers'] ) ) { $headers = $options['security_headers'] ?? array(); $max_score += 10; if ( ! empty( $headers['x_frame_options'] ) ) { $score += 2; } if ( ! empty( $headers['x_content_type_options'] ) ) { $score += 2; } if ( ! empty( $headers['hsts']['enabled'] ) ) { $score += 3; } if ( ! empty( $headers['csp']['enabled'] ) ) { $score += 3; } } // User security details (10 points) if ( ! empty( $options['modules']['user_security'] ) ) { $user = $options['user_security'] ?? array(); $max_score += 10; if ( ! empty( $user['block_insecure_usernames'] ) ) { $score += 3; } if ( ! empty( $user['force_strong_passwords'] ) ) { $score += 3; } if ( ! empty( $user['password_expiration']['enabled'] ) ) { $score += 2; } if ( ! empty( $user['email_verification']['enabled'] ) ) { $score += 2; } } // Audit alerts details (6 points) - only when Security Audit is on, // because the alerting layer rides on top of the activity log. Leaving // both alert legs off keeps these points unearned. if ( ! empty( $options['modules']['activity_log'] ) ) { $alerts = isset( $options['audit_alerts'] ) ? (array) $options['audit_alerts'] : array(); $max_score += 6; if ( Vigilante_Audit_Alerts::immediate_is_active( $alerts ) ) { $score += 3; } if ( Vigilante_Audit_Alerts::threshold_is_active( $alerts ) ) { $score += 3; } } // Environment checks (8 points) - penalize insecure server configuration $max_score += 8; $env_score = 8; // WP_DEBUG active in production is a security risk (exposes paths, errors) if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { $env_score -= 5; } // Accounts with insecure usernames (targeted by brute force attacks) $insecure_admins = $this->get_insecure_admin_usernames(); if ( ! empty( $insecure_admins ) ) { $env_score -= 3; } $score += max( 0, $env_score ); return $max_score > 0 ? round( ( $score / $max_score ) * 100 ) : 0; } /** * Get security recommendations based on current settings * * @param array $options Plugin options. * @return array Array of recommendations. */ private function get_security_recommendations( $options ) { $recommendations = array(); // Critical: Firewall disabled if ( empty( $options['modules']['firewall'] ) ) { $recommendations[] = array( 'icon' => 'warning', 'priority' => 'critical', 'message' => __( 'Enable Firewall to protect against common attacks.', 'vigilante' ), ); } // Critical: Login security disabled if ( empty( $options['modules']['login_security'] ) ) { $recommendations[] = array( 'icon' => 'warning', 'priority' => 'critical', 'message' => __( 'Enable Login Security to prevent brute force attacks.', 'vigilante' ), ); } // High: Security headers disabled if ( empty( $options['modules']['security_headers'] ) ) { $recommendations[] = array( 'icon' => 'admin-generic', 'priority' => 'high', 'message' => __( 'Enable Security Headers to protect against clickjacking and XSS.', 'vigilante' ), ); } // High: User security disabled if ( empty( $options['modules']['user_security'] ) ) { $recommendations[] = array( 'icon' => 'admin-users', 'priority' => 'high', 'message' => __( 'Enable User Security to enforce password policies and username protection.', 'vigilante' ), ); } // High: 2FA not enabled (only if login security is active) $login = $options['login_security'] ?? array(); if ( ! empty( $options['modules']['login_security'] ) && empty( $login['two_factor']['enabled'] ) ) { $recommendations[] = array( 'icon' => 'shield', 'priority' => 'high', 'message' => __( 'Enable Two-Factor Authentication for enhanced login security.', 'vigilante' ), 'tab' => 'login', ); } // Medium: REST API security disabled if ( empty( $options['modules']['rest_api_security'] ) ) { $recommendations[] = array( 'icon' => 'rest-api', 'priority' => 'medium', 'message' => __( 'Enable REST API Security to control API access and prevent enumeration.', 'vigilante' ), ); } // Medium: WP Hardening disabled if ( empty( $options['modules']['wp_hardening'] ) ) { $recommendations[] = array( 'icon' => 'lock', 'priority' => 'medium', 'message' => __( 'Enable WP Hardening to remove version info and protect core files.', 'vigilante' ), ); } // Medium: File integrity disabled if ( empty( $options['modules']['file_integrity'] ) ) { $recommendations[] = array( 'icon' => 'media-text', 'priority' => 'medium', 'message' => __( 'Enable File Integrity to detect unauthorized file changes.', 'vigilante' ), ); } // Medium: Security Audit disabled if ( empty( $options['modules']['activity_log'] ) ) { $recommendations[] = array( 'icon' => 'list-view', 'priority' => 'medium', 'message' => __( 'Enable Security Audit to track security events.', 'vigilante' ), ); } // Low: XML-RPC enabled (only if login security is active) if ( ! empty( $options['modules']['login_security'] ) && empty( $login['disable_xmlrpc'] ) ) { $recommendations[] = array( 'icon' => 'info', 'priority' => 'low', 'message' => __( 'Disable XML-RPC if not needed (reduces attack surface).', 'vigilante' ), 'tab' => 'login', ); } // Low: Strong passwords not enforced (only if user security is active) $user = $options['user_security'] ?? array(); if ( ! empty( $options['modules']['user_security'] ) && empty( $user['force_strong_passwords'] ) ) { $recommendations[] = array( 'icon' => 'admin-users', 'priority' => 'low', 'message' => __( 'Enforce strong passwords for all users.', 'vigilante' ), 'tab' => 'users', ); } // High: WP_DEBUG active in production (regardless of Vigilante settings) if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { $recommendations[] = array( 'icon' => 'warning', 'priority' => 'high', 'message' => __( 'WP_DEBUG is active. Debug mode exposes sensitive information and should be disabled in production.', 'vigilante' ), 'tab' => 'wp-hardening', ); } // Low: Users with display name matching login username (only if user security active) if ( ! empty( $options['modules']['user_security'] ) ) { $exposed_users = $this->get_users_with_exposed_login(); if ( ! empty( $exposed_users ) ) { $recommendations[] = array( 'icon' => 'admin-users', 'priority' => 'low', 'message' => sprintf( /* translators: %s: Comma-separated list of usernames */ __( 'These users have their login username as display name (publicly visible): %s', 'vigilante' ), implode( ', ', $exposed_users ) ), 'tab' => 'users', ); } } // High: Accounts with insecure usernames (regardless of module status) $insecure_admins = $this->get_insecure_admin_usernames(); if ( ! empty( $insecure_admins ) ) { $recommendations[] = array( 'icon' => 'warning', 'priority' => 'high', 'message' => sprintf( /* translators: %s: Comma-separated list of usernames */ __( 'Insecure usernames detected: %s. These are commonly targeted in brute force attacks. Create new accounts with unique usernames and remove these.', 'vigilante' ), implode( ', ', $insecure_admins ) ), ); } // Critical: Closed or removed plugins detected by the daily check. // Reads from the cached state map populated by Vigilante_Plugin_Status, so // there is no extra HTTP call here. Ignored slugs are filtered out so the // recommendation respects the user's per-slug Ignore decisions. if ( ! empty( $options['modules']['file_integrity'] ) && ! empty( $options['file_integrity']['check_closed_plugins'] ) ) { if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) { require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php'; } $closed_checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log ); $closed_active = $closed_checker->get_closed_plugins(); if ( ! empty( $closed_active ) ) { $names = array(); foreach ( $closed_active as $slug => $entry ) { $names[] = isset( $entry['name'] ) ? $entry['name'] : $slug; } $recommendations[] = array( 'icon' => 'warning', 'priority' => 'critical', 'message' => sprintf( /* translators: 1: count, 2: comma-separated plugin names */ _n( '%1$d closed plugin detected on this site: %2$s. Closures in WordPress.org usually indicate malware, security issues or supply chain compromises. Uninstall and replace as soon as possible.', '%1$d closed plugins detected on this site: %2$s. Closures in WordPress.org usually indicate malware, security issues or supply chain compromises. Uninstall and replace as soon as possible.', count( $closed_active ), 'vigilante' ), count( $closed_active ), implode( ', ', $names ) ), 'tab' => 'file-integrity', ); } } // Medium: Security Audit is on but no audit alert is configured. The // alerting layer only makes sense while the activity log is running. if ( ! empty( $options['modules']['activity_log'] ) ) { $alerts = isset( $options['audit_alerts'] ) ? (array) $options['audit_alerts'] : array(); if ( ! Vigilante_Audit_Alerts::has_active_alerts( $alerts ) ) { $recommendations[] = array( 'icon' => 'email-alt', 'priority' => 'medium', 'message' => __( 'Set up Audit Alerts to get an email when something important happens (a new admin, a closed plugin, or an attack in progress).', 'vigilante' ), 'tab' => 'activity-log', ); } } // Sort by priority $priority_order = array( 'critical' => 0, 'high' => 1, 'medium' => 2, 'low' => 3 ); usort( $recommendations, function( $a, $b ) use ( $priority_order ) { return ( $priority_order[ $a['priority'] ] ?? 99 ) - ( $priority_order[ $b['priority'] ] ?? 99 ); } ); return $recommendations; } /** * Get users whose display name matches their login username * * Limited to administrators and editors for performance and relevance. * Cached with transient to avoid repeated queries on every dashboard load. * * @return array Array of usernames with exposed login. */ private function get_users_with_exposed_login() { $cache_key = 'vigilante_exposed_display_names'; $cached = get_transient( $cache_key ); if ( false !== $cached ) { return $cached; } $exposed = array(); $users = get_users( array( 'role__in' => array( 'administrator', 'editor' ), 'fields' => array( 'ID', 'user_login', 'display_name' ), ) ); foreach ( $users as $user ) { if ( strcasecmp( $user->display_name, $user->user_login ) === 0 ) { $exposed[] = $user->user_login; } } // Cache for 12 hours set_transient( $cache_key, $exposed, 12 * HOUR_IN_SECONDS ); return $exposed; } /** * Get accounts with insecure usernames * * Checks for common default usernames that are targeted by brute force attacks. * Detects any user regardless of role (consistent with username creation blocking). * Uses WordPress object cache via get_user_by() so no transient needed. * * @return array Array of insecure usernames found. */ private function get_insecure_admin_usernames() { $priority_usernames = array( 'admin', 'administrator', 'root', 'test', 'user', 'guest', 'info', 'sysadmin', 'webmaster' ); $found = array(); foreach ( $priority_usernames as $username ) { $user = get_user_by( 'login', $username ); if ( $user ) { $found[] = $username; } } return $found; } /** * Get security status for menu badge * * Returns count of disabled modules and whether there are critical issues. * Critical = Firewall or Login Security disabled. * * @return array Array with 'count' and 'has_critical'. */ public function get_security_status_for_badge() { $options = $this->settings->get_all_options(); $modules = $options['modules'] ?? array(); // Count disabled modules $disabled_count = 0; $has_critical = false; // Critical modules - if disabled, badge is red $critical_modules = array( 'firewall', 'login_security' ); foreach ( $modules as $module => $enabled ) { // Handle both boolean and string values ('1', '0', true, false) $is_enabled = filter_var( $enabled, FILTER_VALIDATE_BOOLEAN ); if ( ! $is_enabled ) { $disabled_count++; // Check if this is a critical module if ( in_array( $module, $critical_modules, true ) ) { $has_critical = true; } } } return array( 'count' => $disabled_count, 'has_critical' => $has_critical, ); } /** * Get count of security issues for menu badge (deprecated, use get_security_status_for_badge) * * @return int Count of critical/high issues. */ public function get_security_issues_count() { $status = $this->get_security_status_for_badge(); return $status['count']; } /** * Register settings */ public function register_settings() { register_setting( 'vigilante_options', Vigilante_Settings::OPTION_NAME, array( $this->settings, 'validate_options' ) ); } /** * Build the settings search index * * Flat list of searchable entries consumed by the client-side search. * Each entry contains tab + section metadata, a translated label and an * English fallback so locales with partial translation still match. * * @return array */ private function get_search_index() { return array( // Firewall - Main array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Block bad bots', 'vigilante' ), 'label_en' => 'Block bad bots', 'keywords' => 'bots robots malos bloquear bad block crawlers arañas scrapers' ), array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Block malicious requests', 'vigilante' ), 'label_en' => 'Block malicious requests', 'keywords' => 'malicioso maliciosas peticiones ataques sqli xss rfi lfi ataque exploit injection inyección' ), array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Rate limiting', 'vigilante' ), 'label_en' => 'Rate limiting', 'keywords' => 'limitar tasa velocidad throttle peticiones minuto abuso flood' ), array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Brute force protection', 'vigilante' ), 'label_en' => 'Brute force protection', 'keywords' => 'fuerza bruta brute force contraseñas ataque login diccionario' ), array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'IP Whitelist', 'vigilante' ), 'label_en' => 'IP Whitelist', 'keywords' => 'lista blanca permitida permitidas whitelist allowlist ip direcciones permitir' ), array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'IP Blacklist', 'vigilante' ), 'label_en' => 'IP Blacklist', 'keywords' => 'lista negra bloqueada bloqueadas blacklist blocklist denylist ip direcciones bloquear' ), array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'User-Agent Whitelist', 'vigilante' ), 'label_en' => 'User-Agent Whitelist', 'keywords' => 'ua user agent agente usuario navegador lista blanca permitida whitelist allowlist' ), array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'User-Agent Blacklist', 'vigilante' ), 'label_en' => 'User-Agent Blacklist', 'keywords' => 'ua user agent agente usuario navegador lista negra bloqueada blacklist blocklist denylist' ), // Firewall - Server Protection array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'Directory Browsing', 'vigilante' ), 'label_en' => 'Directory Browsing', 'keywords' => 'directorio navegación listado listing indexing indexado carpetas' ), array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'Protect wp-config.php', 'vigilante' ), 'label_en' => 'Protect wp-config.php', 'keywords' => 'proteger wp-config configuración config archivo' ), array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'field-protect-wp-cron', 'label' => __( 'Protect wp-cron.php', 'vigilante' ), 'label_en' => 'Protect wp-cron.php', 'keywords' => 'proteger wp-cron cron tareas programadas scheduled tasks bloquear block dos abuse spam htaccess' ), array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'Protect wp-includes', 'vigilante' ), 'label_en' => 'Protect wp-includes', 'keywords' => 'proteger wp-includes includes core núcleo archivos' ), array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'PHP in Uploads', 'vigilante' ), 'label_en' => 'PHP in Uploads', 'keywords' => 'php uploads subidas archivos bloquear ejecución' ), array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'Sensitive Files', 'vigilante' ), 'label_en' => 'Sensitive Files', 'keywords' => 'sensibles sensitive files archivos readme license htaccess log' ), array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'Limit HTTP Methods', 'vigilante' ), 'label_en' => 'Limit HTTP Methods', 'keywords' => 'métodos http limit limitar trace options put delete verbs verbos' ), // Security Headers array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'X-Frame-Options', 'vigilante' ), 'label_en' => 'X-Frame-Options', 'keywords' => 'xframe clickjacking iframe cabeceras headers' ), array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'X-Content-Type-Options', 'vigilante' ), 'label_en' => 'X-Content-Type-Options', 'keywords' => 'mime sniffing nosniff content type cabeceras headers' ), array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Referrer-Policy', 'vigilante' ), 'label_en' => 'Referrer-Policy', 'keywords' => 'referer referrer política privacidad cabeceras headers' ), array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'HSTS', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'HSTS', 'vigilante' ), 'label_en' => 'HSTS', 'keywords' => 'strict transport security ssl tls https forzar cabeceras headers' ), array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Content Security Policy', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Content Security Policy', 'vigilante' ), 'label_en' => 'Content Security Policy', 'keywords' => 'csp política seguridad contenido xss scripts inline eval cabeceras headers' ), array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Server Identity', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Server Signature', 'vigilante' ), 'label_en' => 'Server Signature', 'keywords' => 'firma servidor server signature identidad apache nginx ocultar hide fingerprint protección servidor' ), array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Server Identity', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Remove Fingerprinting Headers', 'vigilante' ), 'label_en' => 'Remove Fingerprinting Headers', 'keywords' => 'fingerprint huella identificación cabeceras headers x-powered-by server ocultar eliminar protección servidor' ), // Login Security array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Custom login URL', 'vigilante' ), 'label_en' => 'Custom login URL', 'keywords' => 'url personalizada login acceso entrar wp-login wp-admin slug ocultar esconder' ), array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Two-Factor Authentication', 'vigilante' ), 'label_en' => 'Two-Factor Authentication', 'keywords' => '2fa doble factor autenticación totp google authenticator mfa' ), array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( '2FA', 'vigilante' ), 'label_en' => '2FA', 'keywords' => '2fa doble factor autenticación totp google authenticator mfa two factor' ), array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Failed login attempts', 'vigilante' ), 'label_en' => 'Failed login attempts', 'keywords' => 'intentos fallidos login acceso failed attempts bloqueo bloqueos contraseña errónea' ), array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Lockout', 'vigilante' ), 'label_en' => 'Lockout', 'keywords' => 'bloqueo bloqueado lockout baneo ban duración login' ), // REST API array( 'tab' => 'rest-api', 'tab_label' => __( 'REST API', 'vigilante' ), 'section' => __( 'REST API Security', 'vigilante' ), 'anchor' => 'vigilante-section-rest-api-main', 'label' => __( 'Access Mode', 'vigilante' ), 'label_en' => 'Access Mode', 'keywords' => 'modo acceso rest api público privado autenticado' ), array( 'tab' => 'rest-api', 'tab_label' => __( 'REST API', 'vigilante' ), 'section' => __( 'REST API Security', 'vigilante' ), 'anchor' => 'vigilante-section-rest-api-main', 'label' => __( 'Block User Enumeration', 'vigilante' ), 'label_en' => 'Block User Enumeration', 'keywords' => 'enumeración usuarios users block bloquear autores author slug ?author' ), array( 'tab' => 'rest-api', 'tab_label' => __( 'REST API', 'vigilante' ), 'section' => __( 'REST API Security', 'vigilante' ), 'anchor' => 'vigilante-section-rest-api-main', 'label' => __( 'Disable JSONP', 'vigilante' ), 'label_en' => 'Disable JSONP', 'keywords' => 'jsonp desactivar deshabilitar disable callback' ), // User Security array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Username protection', 'vigilante' ), 'label_en' => 'Username protection', 'keywords' => 'nombre usuario username admin reservado prohibido protección' ), array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Password strength', 'vigilante' ), 'label_en' => 'Password strength', 'keywords' => 'fortaleza fuerza contraseña password débil fuerte complejidad requisitos' ), array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Admin monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-users-admin-monitoring', 'label' => __( 'Admin monitoring', 'vigilante' ), 'label_en' => 'Admin monitoring', 'keywords' => 'monitorización administradores admin supervisión alertas cambios' ), array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Registration approval', 'vigilante' ), 'anchor' => 'vigilante-section-users-registration', 'label' => __( 'Registration approval', 'vigilante' ), 'label_en' => 'Registration approval', 'keywords' => 'aprobación registro registration moderación nuevos usuarios signup' ), array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Session limits', 'vigilante' ), 'anchor' => 'vigilante-section-users-sessions', 'label' => __( 'Session limits', 'vigilante' ), 'label_en' => 'Session limits', 'keywords' => 'sesiones limits límite concurrentes sessions simultáneas' ), array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Password expiration', 'vigilante' ), 'label_en' => 'Password expiration', 'keywords' => 'expiración caducidad contraseña password cambiar renovar rotación' ), array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Email verification', 'vigilante' ), 'anchor' => 'vigilante-section-users-email-verify', 'label' => __( 'Email verification', 'vigilante' ), 'label_en' => 'Email verification', 'keywords' => 'verificación correo email confirmación validación' ), // WP Hardening array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Database Hardening', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-database', 'label' => __( 'Database Hardening', 'vigilante' ), 'label_en' => 'Database Hardening', 'keywords' => 'base datos database db mysql fortalecer hardening endurecer' ), array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Database Hardening', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-database', 'label' => __( 'Database prefix', 'vigilante' ), 'label_en' => 'Database prefix', 'keywords' => 'prefijo base datos database db mysql tabla tablas wp_ cambiar renombrar' ), array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-wpconfig', 'label' => __( 'Disable file editing', 'vigilante' ), 'label_en' => 'Disable file editing', 'keywords' => 'desactivar deshabilitar disable edición editor archivos file edit disallow' ), array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-wpconfig', 'label' => __( 'Disable plugin/theme installation', 'vigilante' ), 'label_en' => 'Disable plugin/theme installation', 'keywords' => 'desactivar deshabilitar disable instalación plugins temas themes install disallow' ), array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-wpconfig', 'label' => __( 'Force SSL admin', 'vigilante' ), 'label_en' => 'Force SSL admin', 'keywords' => 'forzar ssl tls https admin administración certificado' ), array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'field-disable-wp-cron', 'label' => __( 'Disable WP Cron', 'vigilante' ), 'label_en' => 'Disable WP Cron', 'keywords' => 'desactivar deshabilitar disable wp cron tareas programadas scheduled real server crontab pseudo' ), array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Comment Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-comments', 'label' => __( 'Comment Security', 'vigilante' ), 'label_en' => 'Comment Security', 'keywords' => 'comentarios comments spam protección honeypot autores url' ), array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Header Cleanup', 'vigilante' ), 'label_en' => 'Header Cleanup', 'keywords' => 'limpieza cabeceras headers meta tags wordpress generator rsd wlwmanifest' ), array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Remove WordPress version', 'vigilante' ), 'label_en' => 'Remove WordPress version', 'keywords' => 'eliminar quitar versión wordpress wp generator meta ocultar hide' ), array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'field-remove-wp-version-assets', 'label' => __( 'Remove version from assets', 'vigilante' ), 'label_en' => 'Remove version from assets', 'keywords' => 'eliminar quitar versión wordpress wp ver query string assets recursos urls scripts styles css js cache busting' ), array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Disable XML-RPC', 'vigilante' ), 'label_en' => 'Disable XML-RPC', 'keywords' => 'xmlrpc xml-rpc desactivar deshabilitar disable pingback trackback' ), array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'RSS Feed Settings', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-rss', 'label' => __( 'RSS Feed Settings', 'vigilante' ), 'label_en' => 'RSS Feed Settings', 'keywords' => 'rss feed sindicación feeds ajustes configuración' ), // File Integrity array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'File Integrity Monitoring', 'vigilante' ), 'label_en' => 'File Integrity Monitoring', 'keywords' => 'integridad archivos monitorización supervisión hash checksum malware cambios' ), array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Scan schedule', 'vigilante' ), 'label_en' => 'Scan schedule', 'keywords' => 'escaneo escáner programación planificación horario frecuencia cron' ), array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Instant alert', 'vigilante' ), 'label_en' => 'Instant alert', 'keywords' => 'alerta instantánea inmediata notificación aviso email tiempo real' ), array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'Ignored Files', 'vigilante' ), 'anchor' => 'vigilante-section-fi-ignored', 'label' => __( 'Ignored Files', 'vigilante' ), 'label_en' => 'Ignored Files', 'keywords' => 'ignorados ignorar excluir exclusiones archivos ignored exclude' ), // Security Audit array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Security Audit Settings', 'vigilante' ), 'anchor' => 'vigilante-section-audit-settings', 'label' => __( 'Retention', 'vigilante' ), 'label_en' => 'Retention', 'keywords' => 'retención días log registro conservación purga auditoría' ), array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Security Audit Settings', 'vigilante' ), 'anchor' => 'vigilante-section-audit-settings', 'label' => __( 'Events to Log', 'vigilante' ), 'label_en' => 'Events to Log', 'keywords' => 'eventos log registro auditoría registrar capturar' ), array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Security Audit Settings', 'vigilante' ), 'anchor' => 'vigilante-section-audit-settings', 'label' => __( 'Option Tracking', 'vigilante' ), 'label_en' => 'Option Tracking', 'keywords' => 'opciones seguimiento rastreo cambios ajustes options tracking' ), array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Security Audit Settings', 'vigilante' ), 'anchor' => 'vigilante-section-audit-settings', 'label' => __( 'Exclusions', 'vigilante' ), 'label_en' => 'Exclusions', 'keywords' => 'exclusiones excluir ignorar usuarios roles ip filtros' ), array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'vigilante-section-audit-alerts', 'label' => __( 'Audit Alerts', 'vigilante' ), 'label_en' => 'Audit Alerts', 'keywords' => 'alertas avisos aviso notificaciones email correo mail auditoría audit seguridad warning critical destinatarios test prueba' ), array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'field-audit-alerts-immediate', 'label' => __( 'Immediate alerts', 'vigilante' ), 'label_en' => 'Immediate alerts', 'keywords' => 'alertas inmediatas email correo mail aviso severidad crítico critical warning evento auditoría inmediato' ), array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'field-audit-alerts-threshold', 'label' => __( 'Threshold alerts', 'vigilante' ), 'label_en' => 'Threshold alerts', 'keywords' => 'alertas umbral pico ráfaga email correo mail aviso categoría ventana threshold auditoría cortafuegos login' ), array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Recent Activity', 'vigilante' ), 'anchor' => 'vigilante-section-audit-recent', 'label' => __( 'Recent Activity', 'vigilante' ), 'label_en' => 'Recent Activity', 'keywords' => 'actividad reciente log registro eventos últimos historial' ), // Settings & Tools array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Notification settings', 'vigilante' ), 'anchor' => 'vigilante-section-tools-notifications', 'label' => __( 'Notification settings', 'vigilante' ), 'label_en' => 'Notification settings', 'keywords' => 'notificaciones ajustes settings email correo avisos alertas' ), array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Notification settings', 'vigilante' ), 'anchor' => 'vigilante-section-tools-notifications', 'label' => __( 'Additional Recipients', 'vigilante' ), 'label_en' => 'Additional Recipients', 'keywords' => 'destinatarios adicionales correo email cc copia recipients' ), array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Tools', 'vigilante' ), 'anchor' => 'vigilante-section-tools-main', 'label' => __( 'Export Settings', 'vigilante' ), 'label_en' => 'Export Settings', 'keywords' => 'exportar export ajustes configuración settings json' ), array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Tools', 'vigilante' ), 'anchor' => 'vigilante-section-tools-main', 'label' => __( 'Import Settings', 'vigilante' ), 'label_en' => 'Import Settings', 'keywords' => 'importar import ajustes configuración settings json' ), array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Tools', 'vigilante' ), 'anchor' => 'vigilante-section-tools-main', 'label' => __( 'Reset to Defaults', 'vigilante' ), 'label_en' => 'Reset to Defaults', 'keywords' => 'restablecer resetear reset defaults predeterminados valores originales fábrica' ), array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Tools', 'vigilante' ), 'anchor' => 'vigilante-section-tools-main', 'label' => __( 'Create Backup', 'vigilante' ), 'label_en' => 'Create Backup', 'keywords' => 'copia seguridad backup crear generar respaldo' ), array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Tools', 'vigilante' ), 'anchor' => 'vigilante-section-tools-main', 'label' => __( 'Database Backup', 'vigilante' ), 'label_en' => 'Database Backup', 'keywords' => 'base datos database db mysql copia seguridad backup respaldo tablas exportar' ), ); } /** * Enqueue admin assets * * @param string $hook Current admin page. */ public function enqueue_assets( $hook ) { // toplevel_page_vigilante for top-level menu page if ( 'toplevel_page_vigilante' !== $hook ) { return; } wp_enqueue_style( 'vigilante-admin', VIGILANTE_ASSETS_URL . 'css/admin.css', array(), VIGILANTE_VERSION ); wp_enqueue_script( 'vigilante-admin', VIGILANTE_ASSETS_URL . 'js/admin.js', array( 'jquery' ), VIGILANTE_VERSION, true ); wp_localize_script( 'vigilante-admin', 'vigilanteAdmin', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'vigilante_admin_nonce' ), 'currentUserId' => get_current_user_id(), 'logoutUrl' => wp_logout_url( wp_login_url() ), 'adminUrl' => admin_url( 'admin.php?page=vigilante' ), 'searchIndex' => $this->get_search_index(), 'underAttack' => array( 'active' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->is_active(), 'remaining' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->get_remaining_time(), ), 'strings' => array( 'saving' => __( 'Saving...', 'vigilante' ), 'sendingTest' => __( 'Sending...', 'vigilante' ), 'saved' => __( 'Settings saved', 'vigilante' ), 'error' => __( 'Error saving settings', 'vigilante' ), 'confirm' => __( 'Are you sure?', 'vigilante' ), 'scanning' => __( 'Scanning...', 'vigilante' ), 'scanComplete' => __( 'Scan complete', 'vigilante' ), 'loading' => __( 'Loading...', 'vigilante' ), 'searching' => __( 'Searching...', 'vigilante' ), 'noUsersFound' => __( 'No users found', 'vigilante' ), 'searchError' => __( 'Error searching users', 'vigilante' ), 'sending' => __( 'Sending...', 'vigilante' ), 'sendNotification' => __( 'Send notification now', 'vigilante' ), 'notificationsSent' => __( 'notifications sent', 'vigilante' ), 'skipped' => __( 'skipped', 'vigilante' ), 'failed' => __( 'failed', 'vigilante' ), 'customConfig' => __( 'Custom Configuration', 'vigilante' ), // Header tester strings 'testHeaders' => __( 'Test Headers', 'vigilante' ), 'testing' => __( 'Testing...', 'vigilante' ), 'score' => __( 'Score', 'vigilante' ), 'enabledHeaders' => __( 'Enabled headers', 'vigilante' ), 'missingHeaders' => __( 'Missing headers', 'vigilante' ), 'warnings' => __( 'Warnings', 'vigilante' ), // File integrity scan results strings 'scanResults' => __( 'Scan Results', 'vigilante' ), 'ok' => __( 'OK', 'vigilante' ), 'modified' => __( 'Modified', 'vigilante' ), 'suspicious' => __( 'Suspicious', 'vigilante' ), 'totalScanned' => __( 'Total Scanned', 'vigilante' ), 'suspiciousFiles' => __( 'Suspicious Files', 'vigilante' ), 'suspiciousWarning' => __( 'These files may contain malicious code or are in unexpected locations. Review immediately!', 'vigilante' ), 'file' => __( 'File', 'vigilante' ), 'reason' => __( 'Reason', 'vigilante' ), 'type' => __( 'Type', 'vigilante' ), 'unknown' => __( 'Unknown', 'vigilante' ), 'modifiedFiles' => __( 'Modified Files', 'vigilante' ), 'modifiedDescription' => __( 'These files (apparently) differ from the original WordPress or plugin versions.', 'vigilante' ), 'extraFiles' => __( 'Extra Files', 'vigilante' ), 'extra' => __( 'Extra', 'vigilante' ), 'ignored' => __( 'Ignored', 'vigilante' ), 'extraDescription' => __( 'PHP files found in plugins or themes that are not part of the original distribution from WordPress.org.', 'vigilante' ), 'actions' => __( 'Actions', 'vigilante' ), 'ignore' => __( 'Ignore', 'vigilante' ), 'ignoring' => __( 'Ignoring...', 'vigilante' ), 'fileIgnored' => __( 'File added to ignored list.', 'vigilante' ), 'fileUnignored' => __( 'File removed from ignored list.', 'vigilante' ), 'confirmClearIgnored' => __( 'Remove all files from the ignored list? They will appear in scan results again.', 'vigilante' ), 'ignoredCleared' => __( 'Ignored files list cleared. Page will reload...', 'vigilante' ), 'selectAll' => __( 'Select all', 'vigilante' ), 'bulkIgnoreSelected' => __( 'Ignore selected', 'vigilante' ), 'bulkUnignoreSelected'=> __( 'Stop ignoring selected', 'vigilante' ), 'bulkNoSelection' => __( 'Select at least one file first.', 'vigilante' ), 'bulkConfirmIgnore' => __( 'Ignore the selected files? They will be hidden from future scan results until you remove them from the ignored list.', 'vigilante' ), 'bulkConfirmUnignore' => __( 'Remove the selected files from the ignored list? They will appear in scan results again.', 'vigilante' ), 'bulkProcessing' => __( 'Processing...', 'vigilante' ), /* translators: %d: number of files selected for bulk action. */ 'bulkSelectedCount' => __( '%d selected', 'vigilante' ), 'allClear' => __( 'All files verified - no issues found!', 'vigilante' ), 'criticalConfigTitle' => __( 'Critical config files modified', 'vigilante' ), 'criticalConfigDesc' => __( 'These files are common targets for code injection. Review the changes and approve if they are legitimate. Vigilant\'s own blocks are excluded from this check.', 'vigilante' ), 'approve' => __( 'Approve', 'vigilante' ), 'approving' => __( 'Approving...', 'vigilante' ), 'criticalApproved' => __( 'Change approved. Next scan will use the current state as baseline.', 'vigilante' ), 'reviewChanges' => __( 'Review changes', 'vigilante' ), 'hideChanges' => __( 'Hide changes', 'vigilante' ), 'changes' => __( 'Changes', 'vigilante' ), 'diffUnavailable' => __( 'Diff not available for this file (baseline was created before diff tracking was added). Approve to enable diff on future changes.', 'vigilante' ), 'diffEmpty' => __( 'No line-level changes detected (may be whitespace or reordering).', 'vigilante' ), 'diffLines' => __( 'lines', 'vigilante' ), // Under Attack mode strings 'underAttackConfirmActivate' => __( 'Activate Under Attack mode? All visitors will see a verification page for the next 4 hours.', 'vigilante' ), 'underAttackConfirmDeactivate' => __( 'Deactivate Under Attack mode?', 'vigilante' ), 'underAttackActivating' => __( 'Activating...', 'vigilante' ), 'underAttackDeactivating' => __( 'Deactivating...', 'vigilante' ), // Database backup strings 'dbBackupDownloading' => __( 'Generating backup...', 'vigilante' ), 'dbBackupNoTables' => __( 'Please select at least one table.', 'vigilante' ), 'dbBackupSuccess' => __( 'Database backup downloaded successfully.', 'vigilante' ), // Firewall unblock 'confirmUnblockIp' => __( 'Unblock this IP from firewall rate limiting?', 'vigilante' ), // Database prefix strings 'dbPrefixConfirm' => __( 'This operation will change your database prefix. It is irreversible. Make sure you have a current database backup before proceeding.', 'vigilante' ), 'dbPrefixChanging' => __( 'Changing prefix...', 'vigilante' ), 'dbPrefixSuccess' => __( 'Database prefix changed successfully. The page will reload now.', 'vigilante' ), 'dbPrefixCheckbox' => __( 'You must confirm that you have a database backup.', 'vigilante' ), /* translators: 1: Hours, 2: Minutes */ 'underAttackRemaining' => __( '%1$dh %2$dm remaining', 'vigilante' ), 'underAttackLabel' => __( 'Under Attack', 'vigilante' ), 'standardLabel' => __( 'Standard', 'vigilante' ), 'maximumLabel' => __( 'Maximum Security', 'vigilante' ), 'deactivate' => __( 'Deactivate', 'vigilante' ), 'underAttackActivate' => __( 'Activate for 4 hours', 'vigilante' ), // Settings strings 'saveSettings' => __( 'Save Settings', 'vigilante' ), 'settingsResetDefaults' => __( 'Settings reset to defaults.', 'vigilante' ), 'confirmOverwrite' => __( 'This will overwrite your current settings.', 'vigilante' ), 'importFailed' => __( 'Could not import settings. Check the file and try again.', 'vigilante' ), /* translators: 1: tests passed, 2: total tests in this category */ 'testsCounter' => __( '%1$d/%2$d tests', 'vigilante' ), 'confirmResetAll' => __( 'This will reset ALL settings to defaults.', 'vigilante' ), 'couldNotDetermineSection' => __( 'Could not determine section.', 'vigilante' ), 'confirmResetSection' => __( 'Reset this section to default values? This cannot be undone.', 'vigilante' ), 'sectionResetDefaults' => __( 'Section reset to defaults.', 'vigilante' ), /* translators: %s: preset name */ 'confirmApplyPreset' => __( 'Apply the "%s" preset?', 'vigilante' ), // Scan strings 'scanFailed' => __( 'Scan failed', 'vigilante' ), /* translators: %s: error message */ 'scanError' => __( 'Scan error: %s', 'vigilante' ), 'runScanNow' => __( 'Run Scan Now', 'vigilante' ), 'confirmClearScan' => __( 'Are you sure you want to clear all scan results?', 'vigilante' ), 'clearing' => __( 'Clearing...', 'vigilante' ), 'scanResultsCleared' => __( 'Scan results cleared. Page will reload...', 'vigilante' ), 'failedClearResults' => __( 'Failed to clear results', 'vigilante' ), /* translators: %s: error message */ 'ajaxError' => __( 'AJAX Error: %s', 'vigilante' ), // Activity log popup strings 'logRequest' => __( 'Request', 'vigilante' ), 'logDate' => __( 'Date', 'vigilante' ), 'logMethod' => __( 'Method', 'vigilante' ), 'logType' => __( 'Type', 'vigilante' ), 'logAction' => __( 'Action', 'vigilante' ), 'logSeverity' => __( 'Severity', 'vigilante' ), 'logMessage' => __( 'Message', 'vigilante' ), 'logClient' => __( 'Client', 'vigilante' ), 'logUser' => __( 'User', 'vigilante' ), 'logIpAddress' => __( 'IP Address', 'vigilante' ), 'logUserAgent' => __( 'User Agent', 'vigilante' ), 'logIpLabel' => __( 'IP:', 'vigilante' ), 'logUaLabel' => __( 'UA:', 'vigilante' ), 'logWhitelist' => __( 'Whitelist', 'vigilante' ), 'logBlacklist' => __( 'Blacklist', 'vigilante' ), 'logInWhitelist' => __( 'In whitelist', 'vigilante' ), 'logInBlacklist' => __( 'In blacklist', 'vigilante' ), 'logAdded' => __( 'Added!', 'vigilante' ), 'logErrorAddingToList' => __( 'Error adding to list', 'vigilante' ), 'logRequestFailed' => __( 'Request failed', 'vigilante' ), // Activity log table strings 'noLogEntries' => __( 'No log entries found.', 'vigilante' ), 'view' => __( 'View', 'vigilante' ), 'confirmClearLogs' => __( 'This will delete all audit logs.', 'vigilante' ), // Export logs strings 'exporting' => __( 'Exporting...', 'vigilante' ), /* translators: %d: number of entries */ 'logsExported' => __( 'Logs exported (%d entries)', 'vigilante' ), 'noLogsToExport' => __( 'No logs to export', 'vigilante' ), 'exportFailed' => __( 'Export failed', 'vigilante' ), 'exportLogs' => __( 'Export Logs', 'vigilante' ), // Backup strings 'backupCreated' => __( 'Backup created successfully.', 'vigilante' ), 'createBackupNow' => __( 'Download Backup', 'vigilante' ), 'downloadBackup' => __( 'Download Backup (.zip)', 'vigilante' ), /* translators: %d: number of tables */ 'tablesCount' => __( '%d tables', 'vigilante' ), /* translators: 1: table count, 2: human-readable size */ 'dbTablesTotal' => __( '%1$d tables total (%2$s)', 'vigilante' ), /* translators: 1: selected count, 2: human-readable size */ 'dbTablesSelected' => __( '%1$d tables selected (%2$s)', 'vigilante' ), // Settings search strings 'searchNoResults' => __( 'No matching settings found.', 'vigilante' ), 'searchInTab' => __( 'in', 'vigilante' ), // Modules string /* translators: 1: enabled count, 2: total count */ 'modulesEnabled' => __( '%1$d / %2$d modules enabled', 'vigilante' ), // Activity log label maps for JS rendering 'eventTypeLabels' => array( 'login' => __( 'Login', 'vigilante' ), 'user' => __( 'User', 'vigilante' ), 'content' => __( 'Content', 'vigilante' ), 'plugin' => __( 'Plugin', 'vigilante' ), 'theme' => __( 'Theme', 'vigilante' ), 'settings' => __( 'Settings', 'vigilante' ), 'comment' => __( 'Comment', 'vigilante' ), 'media' => __( 'Media', 'vigilante' ), 'firewall' => __( 'Firewall', 'vigilante' ), 'file' => __( 'File', 'vigilante' ), 'security' => __( 'Security', 'vigilante' ), 'system' => __( 'System', 'vigilante' ), ), 'severityLabels' => array( 'info' => __( 'Info', 'vigilante' ), 'warning' => __( 'Warning', 'vigilante' ), 'critical' => __( 'Critical', 'vigilante' ), ), // Password reset strings 'noUsersFoundSearch' => __( 'No users found', 'vigilante' ), /* translators: %d: number of users */ 'confirmForceReset' => __( 'Force password reset for %d user(s)? A password reset email will be sent to each user.', 'vigilante' ), 'warningResettingSelf' => __( 'WARNING: You are including yourself. Your session will end and you will need to set a new password.', 'vigilante' ), 'processing' => __( 'Processing...', 'vigilante' ), 'anErrorOccurred' => __( 'An error occurred', 'vigilante' ), 'forceResetSelected' => __( 'Force Reset for Selected Users', 'vigilante' ), 'confirmForceResetAll' => __( 'This will force ALL users to reset their password. All users will receive a password reset email. Are you sure you want to continue?', 'vigilante' ), 'warningResettingSelfAll' => __( 'WARNING: You are including yourself. Your session will end immediately.', 'vigilante' ), 'forceResetAll' => __( 'Force Reset for ALL Users', 'vigilante' ), // Role-based password reset strings /* translators: %d: number of users */ 'confirmForceResetByRole' => __( 'Force password reset for %d user(s) with the selected roles? A password reset email will be sent to each user.', 'vigilante' ), 'noRolesSelected' => __( 'Please select at least one role.', 'vigilante' ), 'forceResetByRole' => __( 'Force Reset for Selected Roles', 'vigilante' ), // User approval strings 'confirmApprove' => __( 'Approve this user?', 'vigilante' ), 'approve' => __( 'Approve', 'vigilante' ), 'rejectReason' => __( 'Enter rejection reason (optional):', 'vigilante' ), 'reject' => __( 'Reject', 'vigilante' ), 'noPending' => __( 'No pending registrations.', 'vigilante' ), // Session management strings 'confirmRevoke' => __( 'Revoke this session?', 'vigilante' ), 'confirmRevokeAll' => __( 'Revoke all other sessions?', 'vigilante' ), 'revokeOthers' => __( 'Revoke All Other Sessions', 'vigilante' ), 'confirmRevokeAllUser' => __( 'Revoke ALL sessions for this user? They will be logged out everywhere.', 'vigilante' ), 'sessionsFor' => __( 'Sessions for:', 'vigilante' ), 'noSessions' => __( 'No active sessions', 'vigilante' ), 'revoke' => __( 'Revoke', 'vigilante' ), 'noUsers' => __( 'No users found', 'vigilante' ), // Time ago strings 'timeYear' => __( 'year', 'vigilante' ), 'timeYears' => __( 'years', 'vigilante' ), 'timeMonth' => __( 'month', 'vigilante' ), 'timeMonths' => __( 'months', 'vigilante' ), 'timeDay' => __( 'day', 'vigilante' ), 'timeDays' => __( 'days', 'vigilante' ), 'timeHour' => __( 'hour', 'vigilante' ), 'timeHours' => __( 'hours', 'vigilante' ), 'timeMinute' => __( 'minute', 'vigilante' ), 'timeMinutes' => __( 'minutes', 'vigilante' ), /* translators: %1$d: count, %2$s: time unit */ 'timeAgo' => __( '%1$d %2$s ago', 'vigilante' ), 'justNow' => __( 'Just now', 'vigilante' ), // Pagination strings /* translators: 1: first item number, 2: last item number, 3: total items */ 'paginationOf' => __( '%1$d–%2$d of %3$d', 'vigilante' ), 'paginationEmpty' => __( '0 items', 'vigilante' ), // Security Analyzer strings 'analyzerScanNow' => __( 'Scan now', 'vigilante' ), 'analyzerScanning' => __( 'Scanning…', 'vigilante' ), 'analyzerFastPhase' => __( 'Running fast checks…', 'vigilante' ), 'analyzerSlowPhase' => __( 'Running remote checks…', 'vigilante' ), 'analyzerScanComplete' => __( 'Security scan complete.', 'vigilante' ), 'analyzerScanFailed' => __( 'Security scan failed.', 'vigilante' ), 'analyzerShowDetails' => __( 'Show detailed breakdown', 'vigilante' ), 'analyzerHideDetails' => __( 'Hide detailed breakdown', 'vigilante' ), 'analyzerGoToSetting' => __( 'Go to setting', 'vigilante' ), 'analyzerNoData' => __( 'No data yet — run a scan to populate this category.', 'vigilante' ), 'analyzerJustNow' => __( 'just now', 'vigilante' ), 'analyzerAgo' => __( 'ago', 'vigilante' ), 'analyzerSettingsSaved' => __( 'Analyzer settings saved.', 'vigilante' ), 'analyzerLastScanJustNow' => __( 'Last scan just now', 'vigilante' ), 'analyzerQualityExcellent' => __( 'Excellent', 'vigilante' ), 'analyzerQualityGood' => __( 'Good', 'vigilante' ), 'analyzerQualityFair' => __( 'Fair', 'vigilante' ), 'analyzerQualityPoor' => __( 'Poor', 'vigilante' ), 'analyzerQualityCritical' => __( 'Critical', 'vigilante' ), 'analyzerPts' => __( 'pts', 'vigilante' ), 'analyzerLearnMore' => __( 'Learn more', 'vigilante' ), 'analyzerInfoAllClear' => __( 'All clear', 'vigilante' ), /* translators: %d: number of findings in an info-only category */ 'analyzerInfoFindings' => __( '%d findings', 'vigilante' ), ), ) ); // 2FA Admin assets wp_enqueue_style( 'vigilante-2fa-admin', VIGILANTE_ASSETS_URL . 'css/two-factor-admin.css', array( 'vigilante-admin' ), VIGILANTE_VERSION ); wp_enqueue_script( 'vigilante-2fa-admin', VIGILANTE_ASSETS_URL . 'js/two-factor-admin.js', array( 'jquery', 'vigilante-admin' ), VIGILANTE_VERSION, true ); } /** * Show admin notices */ public function show_admin_notices() { // Activation notice if ( get_transient( 'vigilante_activated' ) ) { ?>

0 ) { $ua_hours = floor( $ua_remaining / 3600 ); $ua_mins = floor( ( $ua_remaining % 3600 ) / 60 ); $dashboard_url = admin_url( 'admin.php?page=vigilante' ); ?>

$server_key ) { if ( empty( $_SERVER[ $server_key ] ) ) { continue; } $candidate = sanitize_text_field( wp_unslash( $_SERVER[ $server_key ] ) ); if ( false !== strpos( $candidate, ',' ) ) { $parts = explode( ',', $candidate ); $candidate = trim( $parts[0] ); } if ( ! filter_var( $candidate, FILTER_VALIDATE_IP ) || $candidate === $remote ) { continue; } // Loopback forwarded IP = local development, not a real proxy. if ( '::1' === $candidate || 0 === strpos( $candidate, '127.' ) ) { continue; } $detected = $proxy_label; break; } if ( '' !== $detected ) { $firewall_url = admin_url( 'admin.php?page=vigilante&tab=firewall' ); $detail_message = sprintf( /* translators: %s: detected forwarded header name wrapped in a code tag. */ esc_html__( 'Requests are arriving with a %s header, but visitor IP detection is set to direct connection. The firewall is reading the proxy address instead of the real visitor IP, which affects the IP lists and rate limiting.', 'vigilante' ), '' . esc_html( strtoupper( $detected ) ) . '' ); ?>

current_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'dashboard'; if ( ! array_key_exists( $this->current_tab, $this->tabs ) ) { $this->current_tab = 'dashboard'; } ?>

Vigilante v

render_tabs(); ?>
render_tab_content(); ?>
render_sidebar(); ?>
settings->get_all_options(); // Map tabs to modules $tab_to_module = array( 'firewall' => 'firewall', 'headers' => 'security_headers', 'login' => 'login_security', 'rest-api' => 'rest_api_security', 'users' => 'user_security', 'wp-hardening' => 'wp_hardening', 'file-integrity' => 'file_integrity', 'activity-log' => 'activity_log', ); ?> current_tab ); if ( method_exists( $this, $method ) ) { $this->$method(); } else { $this->render_tab_coming_soon(); } } /** * Render coming soon placeholder */ private function render_tab_coming_soon() { ?>

settings->is_module_enabled( $module_key ) ) { return false; } $module_labels = $this->settings->get_module_labels(); $module_name = isset( $module_labels[ $module_key ] ) ? $module_labels[ $module_key ] : $module_key; ?>

-

label/max). * @param array $analyzer_settings Settings subsection for weekly cron + email. */ private function render_analyzer_widget( $last_scan, $history, $categories_def, $analyzer_settings ) { $has_data = ! empty( $last_scan['ran_at'] ); $score = isset( $last_scan['score'] ) ? (int) $last_scan['score'] : 0; $grade = isset( $last_scan['grade'] ) ? (string) $last_scan['grade'] : ''; $counts = isset( $last_scan['counts'] ) && is_array( $last_scan['counts'] ) ? $last_scan['counts'] : array(); $ran_at_human = $has_data ? sprintf( /* translators: %s: relative time like "2 hours" */ __( 'Last scan %s ago', 'vigilante' ), human_time_diff( (int) $last_scan['ran_at'], time() ) ) : __( 'Never scanned', 'vigilante' ); $categories = isset( $last_scan['categories'] ) && is_array( $last_scan['categories'] ) ? $last_scan['categories'] : array(); $weekly_enabled = ! isset( $analyzer_settings['weekly_scan_enabled'] ) || ! empty( $analyzer_settings['weekly_scan_enabled'] ); $email_enabled = ! empty( $analyzer_settings['email_on_regression'] ); $quality = self::analyzer_quality_tag( $score ); ?>

%

= 3 ) : $current_score = (int) end( $hist_points ); $previous_score = (int) $hist_points[ $hist_count - 2 ]; $delta = $current_score - $previous_score; $delta_class = $delta > 0 ? 'vigilante-analyzer-delta--up' : ( $delta < 0 ? 'vigilante-analyzer-delta--down' : 'vigilante-analyzer-delta--flat' ); $delta_icon = $delta > 0 ? 'arrow-up-alt' : ( $delta < 0 ? 'arrow-down-alt' : 'minus' ); if ( 0 === $delta ) { $delta_text = __( 'No change', 'vigilante' ); } else { $delta_text = sprintf( /* translators: %s: signed delta, e.g. "+3" or "-5" */ _n( '%s pt vs. previous scan', '%s pts vs. previous scan', abs( $delta ), 'vigilante' ), ( $delta > 0 ? '+' : '' ) . (int) $delta ); } ?>

__( 'Excellent', 'vigilante' ), 'slug' => 'a' ); } if ( $pct >= 70 ) { return array( 'label' => __( 'Good', 'vigilante' ), 'slug' => 'b' ); } if ( $pct >= 50 ) { return array( 'label' => __( 'Fair', 'vigilante' ), 'slug' => 'c' ); } if ( $pct >= 30 ) { return array( 'label' => __( 'Poor', 'vigilante' ), 'slug' => 'd' ); } return array( 'label' => __( 'Critical', 'vigilante' ), 'slug' => 'e' ); } /** * Render a single analyzer check row (used both server-side and via JS template). * * @param array $check Check result array (from Vigilante_SA_Check_Result::to_array()). * @return string HTML (escaped). */ private static function render_analyzer_check_row( $check ) { $id = isset( $check['id'] ) ? $check['id'] : ''; $state = isset( $check['state'] ) ? $check['state'] : 'skip'; $label = isset( $check['label'] ) ? $check['label'] : ''; $detail = isset( $check['detail'] ) ? $check['detail'] : ''; $score = isset( $check['score'] ) ? (int) $check['score'] : 0; $max = isset( $check['max'] ) ? (int) $check['max'] : 0; $fix_link = isset( $check['fix_link'] ) ? $check['fix_link'] : ''; $icons = array( 'pass' => 'yes-alt', 'warn' => 'warning', 'fail' => 'dismiss', 'info' => 'info', 'skip' => 'minus', ); $icon = isset( $icons[ $state ] ) ? $icons[ $state ] : 'minus'; $html = '
  • '; $html .= ''; $html .= '
    '; $html .= '
    '; $html .= '' . esc_html( $label ) . ''; if ( $max > 0 && 'info' !== $state && 'skip' !== $state ) { $html .= '' . esc_html( $score . '/' . $max ) . ' ' . esc_html__( 'pts', 'vigilante' ) . '' . ''; } $html .= '
    '; if ( $detail ) { $html .= '

    ' . esc_html( $detail ) . '

    '; } if ( $fix_link && in_array( $state, array( 'fail', 'warn' ), true ) ) { $html .= '' . esc_html__( 'Go to setting', 'vigilante' ) . ''; } elseif ( $fix_link && 'info' === $state ) { // Info rows (e.g. DNSBL lookups) get an external "Learn more" link instead. $is_external = 0 === strpos( $fix_link, 'http' ); $html .= '' . esc_html__( 'Learn more', 'vigilante' ) . ''; } $html .= '
    '; $html .= '
  • '; return $html; } /** * Build a minimal SVG sparkline for the score history. * * @param int[] $points Score values (0..100), oldest to newest. * @return string SVG markup. */ private static function sparkline_svg( $points ) { $points = array_map( 'intval', (array) $points ); $count = count( $points ); if ( $count < 2 ) { return ''; } $width = 280; $height = 60; $padding = 4; $usable_w = $width - ( $padding * 2 ); $usable_h = $height - ( $padding * 2 ); $step = $usable_w / max( 1, $count - 1 ); $max = 100; // Fixed scale — scores are 0..100. $coords = array(); foreach ( $points as $i => $v ) { $x = $padding + ( $i * $step ); $y = $padding + ( $usable_h - ( ( $v / $max ) * $usable_h ) ); $coords[] = round( $x, 2 ) . ',' . round( $y, 2 ); } $path = 'M ' . implode( ' L ', $coords ); $last = end( $points ); $last_x = $padding + ( ( $count - 1 ) * $step ); $last_y = $padding + ( $usable_h - ( ( $last / $max ) * $usable_h ) ); $svg = ''; $svg .= ''; $svg .= ''; $svg .= ''; return $svg; } /** * Render dashboard tab */ private function render_tab_dashboard() { $options = $this->settings->get_all_options(); $module_labels = $this->settings->get_module_labels(); $module_descriptions = $this->settings->get_module_descriptions(); $presets = $this->settings->get_presets(); $active_preset = get_option( 'vigilante_active_preset', '' ); // Calculate security score with more factors $security_score = $this->calculate_security_score( $options ); // Security Analyzer (v2.1.0) — hydrate widget with the last persisted scan, if any. if ( ! class_exists( 'Vigilante_Security_Analyzer' ) ) { require_once VIGILANTE_INCLUDES_DIR . 'class-security-analyzer.php'; } $analyzer_instance = new Vigilante_Security_Analyzer( $this->settings, $this->activity_log ); $analyzer_last_scan = $analyzer_instance->get_last_scan(); $analyzer_history = $analyzer_instance->get_score_history(); $analyzer_categories_def = Vigilante_Security_Analyzer::get_categories(); $analyzer_settings = isset( $options['security_analyzer'] ) ? $options['security_analyzer'] : array(); ?>

    = 90 ) { $grade = 'A'; } elseif ( $security_score >= 70 ) { $grade = 'B'; } elseif ( $security_score >= 50 ) { $grade = 'C'; } elseif ( $security_score >= 30 ) { $grade = 'D'; } else { $grade = 'E'; } ?>
    %
    get_security_recommendations( $options ); if ( ! empty( $recommendations ) ) : ?>

    render_analyzer_widget( $analyzer_last_scan, $analyzer_history, $analyzer_categories_def, $analyzer_settings ); ?>

    $enabled ) : $label = isset( $module_labels[ $module ] ) ? $module_labels[ $module ] : ucwords( str_replace( '_', ' ', $module ) ); $description = isset( $module_descriptions[ $module ] ) ? $module_descriptions[ $module ] : ''; ?>

    $preset ) : $is_active = ( $active_preset === $preset_id ); ?>

    settings, $this->activity_log ); $ua_active = $under_attack->is_active(); $ua_remaining = $under_attack->get_remaining_time(); $ua_remaining_hours = floor( $ua_remaining / 3600 ); $ua_remaining_mins = floor( ( $ua_remaining % 3600 ) / 60 ); ?>

    settings->get_section( 'email' ); $login_options = $this->settings->get_section( 'login_security' ); $user_options = $this->settings->get_section( 'user_security' ); $fi_options = $this->settings->get_section( 'file_integrity' ); $alerts_options = $this->settings->get_section( 'audit_alerts' ); $monitoring = $user_options['admin_monitoring'] ?? array(); $registration = $user_options['registration_approval'] ?? array(); $current_admin = get_option( 'admin_email' ); $send_to_admin = ! isset( $email_options['send_to_admin_email'] ) || ! empty( $email_options['send_to_admin_email'] ); $additional_raw = $email_options['additional_recipients'] ?? array(); $additional = is_array( $additional_raw ) ? implode( "\n", $additional_raw ) : trim( $additional_raw ); ?>

    __( 'Login lockout', 'vigilante' ), 'active' => ! empty( $login_options['notify_on_lockout'] ), 'tab' => 'login', ), array( 'label' => __( 'Administrator login', 'vigilante' ), 'active' => ! empty( $login_options['notify_on_admin_login'] ), 'tab' => 'login', ), array( 'label' => __( 'New administrator created', 'vigilante' ), 'active' => ! empty( $monitoring['alert_new_admin'] ), 'tab' => 'users', ), array( 'label' => __( 'Administrator email changed', 'vigilante' ), 'active' => ! empty( $monitoring['alert_admin_email_change'] ), 'tab' => 'users', ), array( 'label' => __( 'Permission elevation', 'vigilante' ), 'active' => ! empty( $monitoring['alert_permission_elevation'] ), 'tab' => 'users', ), array( 'label' => __( 'Admin password changed', 'vigilante' ), 'active' => ! empty( $monitoring['alert_admin_password_change'] ), 'tab' => 'users', ), array( 'label' => __( 'Registration pending approval', 'vigilante' ), 'active' => ! empty( $registration['enabled'] ) && ! empty( $registration['notify_admin'] ), 'tab' => 'users', ), array( 'label' => __( 'File integrity scan report', 'vigilante' ), 'active' => ( $fi_options['notify_level'] ?? 'disabled' ) !== 'disabled', 'tab' => 'file-integrity', ), array( 'label' => __( 'File integrity instant alert', 'vigilante' ), 'active' => ! empty( $fi_options['instant_alert'] ), 'tab' => 'file-integrity', ), array( 'label' => __( 'Audit alert: immediate', 'vigilante' ), 'active' => Vigilante_Audit_Alerts::immediate_is_active( $alerts_options ), 'tab' => 'activity-log', 'anchor' => 'vigilante-section-audit-alerts', ), array( 'label' => __( 'Audit alert: threshold', 'vigilante' ), 'active' => Vigilante_Audit_Alerts::threshold_is_active( $alerts_options ), 'tab' => 'activity-log', 'anchor' => 'vigilante-section-audit-alerts', ), array( 'label' => __( 'Under Attack mode', 'vigilante' ), 'active' => true, 'tab' => '', 'note' => __( 'Always active', 'vigilante' ), ), array( 'label' => __( 'Plugin deactivation', 'vigilante' ), 'active' => ! empty( $email_options['send_deactivation_email'] ), 'tab' => 'tools', ), ); foreach ( $notifications as $notif ) : $status_class = $notif['active'] ? 'vigilante-status-active' : 'vigilante-status-inactive'; $status_label = $notif['active'] ? __( 'Active', 'vigilante' ) : __( 'Inactive', 'vigilante' ); if ( ! empty( $notif['note'] ) ) { $status_label = $notif['note']; } ?>

    render_module_disabled_notice( 'firewall' ); $options = $this->settings->get_section( 'firewall' ); ?>
    >

    $block_data ) : ?>

    ' . esc_html( $this->database->get_client_ip() ) . '' ); ?>


    , 2: closing . Placeholders wrap the IP, CIDR and wildcard examples. */ esc_html__( 'Accepts exact IPs (%1$s192.168.1.50%2$s), CIDR ranges (%1$s192.168.1.0/24%2$s, IPv4 and IPv6), and wildcards with %1$s*%2$s (e.g. %1$s192.168.1.*%2$s).', 'vigilante' ), '', '' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded. ?>


    , 2: closing . Placeholders wrap the IP, CIDR and wildcard examples. */ esc_html__( 'Accepts exact IPs (%1$s203.0.113.42%2$s), CIDR ranges (%1$s203.0.113.0/24%2$s, IPv4 and IPv6), and wildcards with %1$s*%2$s (e.g. %1$s203.0.113.*%2$s).', 'vigilante' ), '', '' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded. ?>

    , 2: closing */ esc_html__( '%1$sWarning:%2$s Only enable if your host runs a real server-side cron job calling wp-cron.php (most managed WordPress hosts do; check with your provider). Otherwise scheduled tasks — publishing, updates, emails, backups — stop running. Pair with %3$sDISABLE_WP_CRON%4$s in WP Hardening for full coverage.', 'vigilante' ), '', '', '', '' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded. ?>

    render_module_disabled_notice( 'login_security' ); $options = $this->settings->get_section( 'login_security' ); $lockouts = $this->database->get_active_lockouts(); ?>
    >

    render_2fa_settings( $options ); ?>

    ' . esc_html__( 'Settings & Tools', 'vigilante' ) . '' ); ?>

    render_lockout_info_section( $options, $lockouts ); ?>

    '; printf( /* translators: %d: Maximum lockout duration in hours */ esc_html__( 'Progressive lockout enabled (max: %d hours).', 'vigilante' ), absint( ceil( $max_lockout / 3600 ) ) ); } if ( $two_factor_enabled ) { echo '
    '; esc_html_e( 'Failed 2FA codes also count toward the lockout limit.', 'vigilante' ); } ?>
    locked_until ); $remaining_seconds = $lockout_time - time(); $remaining_text = $this->format_remaining_time( $remaining_seconds ); ?>
    ip_address ); ?> attempts ); ?>

    0 ) { return sprintf( /* translators: 1: Number of hours, 2: Number of minutes */ __( '%1$d hours %2$d minutes', 'vigilante' ), $hours, $remaining_minutes ); } return sprintf( /* translators: %d: Number of hours */ _n( '%d hour', '%d hours', $hours, 'vigilante' ), $hours ); } /** * Render 2FA settings section * * @param array $options Login security options. */ private function render_2fa_settings( $options ) { $two_factor = $options['two_factor'] ?? array(); $all_roles = wp_roles()->roles; $enforced = $two_factor['enforced_roles'] ?? array( 'administrator', 'editor' ); $excluded = $two_factor['excluded_users'] ?? array(); $method = $two_factor['method'] ?? 'email'; $grace_days = $two_factor['grace_period_days'] ?? 3; ?>

    > > >
    $role_data ) : $user_count = count( get_users( array( 'role' => $role_slug, 'fields' => 'ID' ) ) ); ?>

    display_name . ' (' . $user->user_email . ')' ); ?>

    render_module_disabled_notice( 'security_headers' ); $options = $this->settings->get_section( 'security_headers' ); ?>
    >

    render_module_disabled_notice( 'rest_api_security' ); $options = $this->settings->get_section( 'rest_api_security' ); ?>
    >

    render_module_disabled_notice( 'user_security' ); $options = $this->settings->get_section( 'user_security' ); $monitoring = $options['admin_monitoring'] ?? array(); $registration = $options['registration_approval'] ?? array(); $session_limits = $options['session_limits'] ?? array(); $password_exp = $options['password_expiration'] ?? array(); $email_verify = $options['email_verification'] ?? array(); ?>
    >

    true, 'require_lowercase' => true, 'require_number' => true, 'require_special' => true, 'block_common' => true, 'block_username' => false, 'affected_roles' => array(), ) ); $pw_policy_roles = (array) $pw_policy['affected_roles']; ?>

    get_names() as $role_slug => $role_name ) : ?>

    ' . esc_html__( 'Settings & Tools', 'vigilante' ) . '' ); ?>

    get_names(); foreach ( $all_roles as $role_slug => $role_name ) : ?>
    display_name . ' (' . $excluded_user->user_email . ')' ); ?>

    roles; ?>
    roles as $role_slug => $role_data ) : $count = $avail_roles[ $role_slug ] ?? 0; if ( 0 === $count ) { continue; } $role_name = translate_user_role( $role_data['name'] ); ?>

    settings, $this->activity_log ); $pending_users = $user_security->get_pending_users(); ?>

    0 ) : ?>

    ID, 'vigilante_pending_since', true ); ?>
    ID, 32 ); ?> user_login ); ?> user_email ); ?> user_registered ); } ?>

    get_user_sessions( $current_user_id ); $has_corrupted = $user_security->has_corrupted_sessions( $current_user_id ); $raw_count = $user_security->get_raw_session_count( $current_user_id ); ?> 0 ) : ?>

    1 || $has_corrupted ) : ?>

    render_module_disabled_notice( 'wp_hardening' ); $options = $this->settings->get_section( 'wp_hardening' ); ?>
    >

    get_current_prefix(); $is_default = $db_prefix->is_default_prefix(); ?>
    generate_prefix() ); ?>

    ' . esc_html__( 'Download a database backup', 'vigilante' ) . '' ); ?>

    , 2: closing , 3: opening , 4: closing */ esc_html__( '%1$sWarning:%2$s Only enable if your host runs a real server-side cron job calling wp-cron.php. Otherwise scheduled tasks stop running. This constant only stops the page-view auto-spawn — to also block external HTTP abuse, enable %3$sProtect wp-cron.php%4$s in Firewall → File Protection.', 'vigilante' ), '', '', '', '' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded. ?>

    render_module_disabled_notice( 'activity_log' ); $options = $this->settings->get_section( 'activity_log' ); ?>
    >

      





    settings->get_section( 'audit_alerts' ); $immediate = isset( $alerts['immediate'] ) ? $alerts['immediate'] : array(); $threshold = isset( $alerts['threshold'] ) ? $alerts['threshold'] : array(); $alert_severity = isset( $immediate['min_severity'] ) ? $immediate['min_severity'] : 'critical'; $alert_window = isset( $threshold['window'] ) ? $threshold['window'] : '1h'; $threshold_cats = isset( $threshold['categories'] ) ? (array) $threshold['categories'] : array(); $cat_labels = Vigilante_Audit_Alerts::category_labels(); ?>
    >

    $cat_label ) : $cat_value = isset( $threshold_cats[ $cat_slug ] ) ? (int) $threshold_cats[ $cat_slug ] : 0; ?>

    ' . esc_html__( 'Settings & Tools', 'vigilante' ) . '' ); ?>

    activity_log->get_logs( array( 'per_page' => 20 ) ); $total_logs = $this->activity_log->get_logs_count(); // Label maps for translated display $type_labels = array( 'login' => __( 'Login', 'vigilante' ), 'user' => __( 'User', 'vigilante' ), 'content' => __( 'Content', 'vigilante' ), 'plugin' => __( 'Plugin', 'vigilante' ), 'theme' => __( 'Theme', 'vigilante' ), 'settings' => __( 'Settings', 'vigilante' ), 'comment' => __( 'Comment', 'vigilante' ), 'media' => __( 'Media', 'vigilante' ), 'firewall' => __( 'Firewall', 'vigilante' ), 'file' => __( 'File', 'vigilante' ), 'security' => __( 'Security', 'vigilante' ), 'system' => __( 'System', 'vigilante' ), ); $severity_labels = array( 'info' => __( 'Info', 'vigilante' ), 'warning' => __( 'Warning', 'vigilante' ), 'critical' => __( 'Critical', 'vigilante' ), ); $firewall_options = $this->settings->get_section( 'firewall' ); $ip_whitelist = $firewall_options['ip_whitelist'] ?? array(); $ip_blacklist = $firewall_options['ip_blacklist'] ?? array(); $ua_whitelist = $firewall_options['ua_whitelist'] ?? array(); $ua_blacklist = $firewall_options['ua_blacklist'] ?? array(); ?>
    20 ) : ?> 0 ? 1 : 0, absint( $showing ), absint( $total_logs ) ); ?> 20 ) : ?>
    request_method ) ? $log->request_method : ''; // Prepare details as a simple object $ip_val = (string) ( $log->ip_address ?? '' ); $ua_val = (string) ( $log->user_agent ?? '' ); $details = array( 'id' => (int) $log->id, 'type' => (string) ( $log->event_type ?? '' ), 'action' => (string) ( $log->event_action ?? '' ), 'message' => (string) ( $log->event_message ?? '' ), 'user' => (string) ( $log->user_login ?? '' ), 'ip' => $ip_val, 'user_agent' => $ua_val, 'request_method' => (string) $request_method, 'date' => (string) ( $log->created_at ?? '' ), 'severity' => (string) ( $log->severity ?? 'info' ), 'is_ip_whitelisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_whitelist, true ) ), 'is_ip_blacklisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_blacklist, true ) ), 'is_ua_whitelisted' => ( '' !== $ua_val && in_array( $ua_val, $ua_whitelist, true ) ), 'is_ua_blacklisted' => ( '' !== $ua_val && in_array( $ua_val, $ua_blacklist, true ) ), ); $display_type = isset( $type_labels[ $log->event_type ] ) ? $type_labels[ $log->event_type ] : $log->event_type; $display_severity = isset( $severity_labels[ $log->severity ] ) ? $severity_labels[ $log->severity ] : $log->severity; ?>
    created_at ); ?> - event_message ); ?> user_login ?? '-' ); ?> ip_address ); ?>

    render_module_disabled_notice( 'file_integrity' ); $options = $this->settings->get_section( 'file_integrity' ); $last_scan = get_option( 'vigilante_last_integrity_scan' ); $last_results = get_option( 'vigilante_last_integrity_results' ); $ignored_files = get_option( 'vigilante_ignored_files', array() ); // Backward compat: convert old notify_on_changes to notify_level $notify_level = $options['notify_level'] ?? ''; if ( empty( $notify_level ) ) { $notify_level = ! empty( $options['notify_on_changes'] ) ? 'all' : 'disabled'; } // Closed + Removed plugins data. Surfaced inside Last Scan Results so the // user sees file findings and plugin closures together (same tier of risk, // same UI), and as the trigger to keep Last Scan Results open even when no // file scan has run yet (the daily cron may have populated this section). // // Gating by last_check_time > 0 is how "Clear Previous Results" visually // resets this block: the option vigilante_plugin_status_last_check is // deleted on Clear, but the state map and the ignored list survive so the // next scan reconstructs without degrading a 'removed' slug. While // last_check is 0, we treat the plugin_status data as if it didn't exist. if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) { require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php'; } $closed_checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log ); $closed_last_check = $closed_checker->get_last_check_time(); if ( $closed_last_check > 0 ) { $closed_plugins = $closed_checker->get_closed_plugins(); $ignored_closed_plugins = $closed_checker->get_ignored_closed_plugins(); } else { $closed_plugins = array(); $ignored_closed_plugins = array(); } $has_closed = ! empty( $closed_plugins ); $datetime_format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' ); ?>
    >

    ' . esc_html__( 'Settings & Tools', 'vigilante' ) . '' ); ?>






    0 ) : ?>

    0 ) { printf( /* translators: 1: date and time of last scan, 2: formatted file count */ esc_html__( 'Last scan: %1$s (%2$s files scanned)', 'vigilante' ), esc_html( wp_date( $datetime_format, $last_scan ) ), esc_html( number_format_i18n( $scanned_total ) ) ); } else { printf( /* translators: %s: date and time of last scan */ esc_html__( 'Last scan: %s', 'vigilante' ), esc_html( wp_date( $datetime_format, $last_scan ) ) ); } if ( $closed_last_check > 0 && $closed_last_check !== (int) $last_scan ) { echo ' · '; printf( /* translators: %s: date and time of last closed plugins check */ esc_html__( 'Closed plugins last checked: %s', 'vigilante' ), esc_html( wp_date( $datetime_format, $closed_last_check ) ) ); } ?>

    0 ) : ?>

    + -

    $cp_entry ) : $cp_state = $cp_entry['state'] ?? ''; $cp_state_label = 'closed' === $cp_state ? __( 'Closed', 'vigilante' ) : __( 'Removed', 'vigilante' ); $cp_state_color = 'closed' === $cp_state ? '#d63638' : '#b32d2e'; $cp_reason = ''; if ( ! empty( $cp_entry['closed_reason_text'] ) ) { $cp_reason = $cp_entry['closed_reason_text']; } elseif ( 'removed' === $cp_state ) { $cp_reason = __( 'Removed from repository (metadata hidden, typical of Security Issue closures)', 'vigilante' ); } $cp_detected = isset( $cp_entry['first_detected'] ) ? (int) $cp_entry['first_detected'] : 0; ?>

    0 ? esc_html( wp_date( $datetime_format, $cp_detected ) ) : '—'; ?>

    $icp_entry ) : $icp_state = $icp_entry['state'] ?? ''; $icp_state_label = 'closed' === $icp_state ? __( 'Closed', 'vigilante' ) : __( 'Removed', 'vigilante' ); ?>

    render(); } /** * AJAX: Download a ZIP backup of the critical config files. * * Streams wp-config.php and .htaccess (and robots.txt if present) as a * downloadable archive. Config backups are no longer left as files under the * web root, so this hands the admin the archive directly. */ public function ajax_download_files_backup() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Permission denied.', 'vigilante' ), 403 ); } $backup_manager = new Vigilante_Backup_Manager(); $result = $backup_manager->stream_files_zip(); // stream_files_zip() exits on success; only a WP_Error returns here. if ( is_wp_error( $result ) ) { wp_die( esc_html( $result->get_error_message() ), 500 ); } } /** * AJAX: Save settings */ public function ajax_save_settings() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } $section = isset( $_POST['section'] ) ? sanitize_key( $_POST['section'] ) : ''; // Handle $_POST['data'] based on type if ( isset( $_POST['data'] ) && is_array( $_POST['data'] ) ) { $data = map_deep( wp_unslash( $_POST['data'] ), 'sanitize_text_field' ); } elseif ( isset( $_POST['data'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $raw_data = wp_unslash( $_POST['data'] ); parse_str( $raw_data, $data ); $data = map_deep( $data, 'sanitize_textarea_field' ); } else { $data = array(); } if ( empty( $section ) ) { wp_send_json_error( __( 'Invalid section.', 'vigilante' ) ); } // Check if 2FA is being enabled or method changed (for notification sending) $send_2fa_notification = false; $send_login_url_notification = false; if ( 'login_security' === $section ) { // Read old state from DB $db_options = get_option( Vigilante_Settings::OPTION_NAME, array() ); $old_2fa_enabled = ! empty( $db_options['login_security']['two_factor']['enabled'] ); $old_method = $db_options['login_security']['two_factor']['method'] ?? 'email'; // Check new state from submitted data $new_2fa_enabled = false; $notify_on_enable = false; $new_method = isset( $data['login_security']['two_factor']['method'] ) ? sanitize_key( $data['login_security']['two_factor']['method'] ) : 'email'; if ( isset( $data['login_security']['two_factor']['enabled'] ) ) { $new_2fa_enabled = filter_var( $data['login_security']['two_factor']['enabled'], FILTER_VALIDATE_BOOLEAN ); } if ( isset( $data['login_security']['two_factor']['notify_on_enable'] ) ) { $notify_on_enable = filter_var( $data['login_security']['two_factor']['notify_on_enable'], FILTER_VALIDATE_BOOLEAN ); } // Send notification if: // 1. 2FA is being enabled (was off, now on) OR // 2. Method changed while 2FA is enabled if ( $notify_on_enable && $new_2fa_enabled ) { if ( ! $old_2fa_enabled || ( $old_2fa_enabled && $old_method !== $new_method ) ) { $send_2fa_notification = true; } } // Check if login URL changed and notification is enabled $old_login_url = $db_options['login_security']['custom_login_url'] ?? ''; $new_login_url = isset( $data['login_security']['custom_login_url'] ) ? sanitize_title( $data['login_security']['custom_login_url'] ) : ''; $notify_on_url_change = isset( $data['login_security']['notify_on_login_url_change'] ) ? filter_var( $data['login_security']['notify_on_login_url_change'], FILTER_VALIDATE_BOOLEAN ) : false; if ( $notify_on_url_change && ! empty( $new_login_url ) && $new_login_url !== $old_login_url ) { $send_login_url_notification = true; } } // Get defaults $defaults = $this->settings->get_default_options(); // Read ONLY saved options from database (not merged with defaults) $saved_options = get_option( Vigilante_Settings::OPTION_NAME, array() ); // Handle modules if ( 'modules' === $section && isset( $data['modules'] ) ) { if ( ! isset( $saved_options['modules'] ) ) { $saved_options['modules'] = array(); } foreach ( $data['modules'] as $module => $enabled ) { $module = sanitize_key( $module ); $saved_options['modules'][ $module ] = in_array( $enabled, array( '1', 1, 'true', true ), true ); } // Clear active preset when modules change update_option( 'vigilante_active_preset', '' ); } else { // Process primary section if ( isset( $data[ $section ] ) && is_array( $data[ $section ] ) ) { $section_defaults = isset( $defaults[ $section ] ) ? $defaults[ $section ] : array(); $current_section = isset( $saved_options[ $section ] ) ? $saved_options[ $section ] : array(); // Process the submitted data $processed = $this->process_section_data( $data[ $section ], $section_defaults, $current_section ); // Save the processed section $saved_options[ $section ] = $processed; // Clear active preset when any section settings change update_option( 'vigilante_active_preset', '' ); } } // Clear cache before saving wp_cache_delete( Vigilante_Settings::OPTION_NAME, 'options' ); // Save to database update_option( Vigilante_Settings::OPTION_NAME, $saved_options ); // Clear the settings cache $this->settings->clear_cache(); // Apply changes based on section $this->apply_section_changes( $section, $saved_options ); // Send 2FA notifications after settings are saved $notification_result = null; if ( $send_2fa_notification ) { $notification_result = $this->send_2fa_enable_notifications(); } // Send login URL notifications after settings are saved $login_url_result = null; if ( $send_login_url_notification ) { $login_url_result = $this->send_login_url_notifications(); } // Build success message $message = __( 'Settings saved successfully.', 'vigilante' ); if ( $notification_result && $notification_result['sent'] > 0 ) { $message .= ' ' . sprintf( /* translators: %d: Number of emails sent */ _n( '2FA notification sent to %d user.', '2FA notifications sent to %d users.', $notification_result['sent'], 'vigilante' ), $notification_result['sent'] ); } if ( $login_url_result && $login_url_result['sent'] > 0 ) { $message .= ' ' . sprintf( /* translators: %d: Number of emails sent */ _n( 'Login URL notification sent to %d user.', 'Login URL notifications sent to %d users.', $login_url_result['sent'], 'vigilante' ), $login_url_result['sent'] ); } wp_send_json_success( $message ); } /** * Send 2FA enable notifications to users * * @return array Result with 'sent' and 'failed' counts. */ private function send_2fa_enable_notifications() { $result = array( 'sent' => 0, 'failed' => 0, ); // Get settings for roles and method $login_security = $this->settings->get_section( 'login_security' ); $two_factor = isset( $login_security['two_factor'] ) ? $login_security['two_factor'] : array(); $roles = isset( $two_factor['enforced_roles'] ) ? $two_factor['enforced_roles'] : array( 'administrator' ); $method = isset( $two_factor['method'] ) ? $two_factor['method'] : 'email'; if ( empty( $roles ) ) { $roles = array( 'administrator' ); } $excluded = isset( $two_factor['excluded_users'] ) ? array_map( 'absint', $two_factor['excluded_users'] ) : array(); // Get users with these roles $args = array( 'role__in' => $roles, ); if ( ! empty( $excluded ) ) { // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Small excluded users list from settings. $args['exclude'] = $excluded; } $users = get_users( $args ); if ( empty( $users ) ) { return $result; } $site_name = get_bloginfo( 'name' ); $from_name = ! empty( $two_factor['email_from_name'] ) ? $two_factor['email_from_name'] : $site_name; if ( 'totp' === $method ) { // Use TOTP class for styled HTML emails if ( ! class_exists( 'Vigilante_Two_Factor_TOTP' ) ) { require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-totp.php'; } $totp = new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log ); foreach ( $users as $user ) { // Skip users who already have TOTP configured $totp_data = $this->database->get_totp_data( $user->ID ); if ( $totp_data && ! empty( $totp_data['is_configured'] ) ) { continue; } $sent = $totp->send_activation_email( $user, $site_name, $from_name ); if ( $sent ) { $this->database->mark_2fa_notified( $user->ID ); $result['sent']++; } else { $result['failed']++; } } } else { // Email method - use existing email 2FA class if ( ! class_exists( 'Vigilante_Two_Factor_Email' ) ) { require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-email.php'; } $email_2fa = new Vigilante_Two_Factor_Email( $this->settings, $this->database, $this->activity_log ); return $email_2fa->send_activation_notifications( false ); } return $result; } /** * Send login URL change notifications to users with admin access * * @return array Result with 'sent' and 'failed' counts. */ private function send_login_url_notifications() { $login_options = $this->settings->get_section( 'login_security' ); $custom_url = ! empty( $login_options['custom_login_url'] ) ? sanitize_title( $login_options['custom_login_url'] ) : ''; if ( empty( $custom_url ) ) { return array( 'sent' => 0, 'failed' => 0 ); } $login_url = home_url( $custom_url . '/' ); $site_name = get_bloginfo( 'name' ); $admin_roles = array( 'administrator', 'editor', 'author', 'contributor' ); $users = get_users( array( 'role__in' => $admin_roles ) ); if ( empty( $users ) ) { return array( 'sent' => 0, 'failed' => 0 ); } $subject = sprintf( /* translators: %s: Site name */ __( '[%s] Your login URL has changed', 'vigilante' ), $site_name ); $body = Vigilante_Email_Template::p( __( 'The login URL for the admin area has been changed. Please save the new URL below and use it from now on.', 'vigilante' ) ); $body .= Vigilante_Email_Template::url_box( $login_url, __( 'Your new login URL:', 'vigilante' ) ); $body .= Vigilante_Email_Template::alert_box( __( 'The old login address (wp-login.php) will no longer work.', 'vigilante' ) ); $body .= Vigilante_Email_Template::button( $login_url, __( 'Go to login', 'vigilante' ) ); $sent = 0; $failed = 0; foreach ( $users as $user ) { $result = Vigilante_Email_Template::send( $user->user_email, $subject, __( 'Login URL changed', 'vigilante' ), $body ); if ( $result ) { $sent++; } else { $failed++; } } if ( $this->activity_log ) { $this->activity_log->log( 'login', 'login_url_notified', sprintf( /* translators: 1: Sent count, 2: Failed count */ __( 'Login URL notification sent on save: %1$d sent, %2$d failed', 'vigilante' ), $sent, $failed ) ); } return array( 'sent' => $sent, 'failed' => $failed ); } /** * Process section data maintaining proper types from defaults * * @param array $submitted_data Data submitted from form. * @param array $defaults Default values for this section. * @param array $current Current saved values. * @param string $section_name Section name for special handling. * @return array Processed data. */ private function process_section_data( $submitted_data, $defaults, $current, $section_name = '' ) { // Start with defaults, then merge current saved values $result = array_replace_recursive( $defaults, $current ); // Process each submitted value foreach ( $submitted_data as $key => $value ) { $key = sanitize_key( $key ); if ( is_array( $value ) ) { // Nested array (like rate_limiting) $nested_defaults = isset( $defaults[ $key ] ) && is_array( $defaults[ $key ] ) ? $defaults[ $key ] : array(); $nested_current = isset( $result[ $key ] ) && is_array( $result[ $key ] ) ? $result[ $key ] : array(); $result[ $key ] = $this->process_section_data( $value, $nested_defaults, $nested_current, $key ); } else { // Determine type from default value $default_value = isset( $defaults[ $key ] ) ? $defaults[ $key ] : null; if ( null === $default_value ) { // No default, check current value type or use as string $current_value = isset( $current[ $key ] ) ? $current[ $key ] : null; if ( is_bool( $current_value ) ) { $result[ $key ] = in_array( $value, array( '1', 1, 'true', true ), true ); } elseif ( is_int( $current_value ) ) { $result[ $key ] = intval( $value ); } elseif ( is_array( $current_value ) ) { $result[ $key ] = is_string( $value ) ? array_filter( array_map( 'trim', explode( "\n", $value ) ) ) : (array) $value; } else { $result[ $key ] = sanitize_text_field( $value ); } } elseif ( is_bool( $default_value ) ) { // Boolean: '1', 1, 'true' become true; '0', 0, '', 'false' become false $result[ $key ] = in_array( $value, array( '1', 1, 'true', true ), true ); } elseif ( is_int( $default_value ) ) { // Integer - with special handling for time fields shown in minutes $int_value = intval( $value ); // Convert minutes to seconds for login_security duration fields // These are displayed as minutes in the form but stored as seconds if ( in_array( $key, array( 'lockout_duration', 'max_lockout_duration' ), true ) ) { $int_value = $int_value * 60; } $result[ $key ] = $int_value; } elseif ( is_array( $default_value ) ) { // Array from textarea (e.g., IP lists) if ( is_string( $value ) ) { $result[ $key ] = array_filter( array_map( 'trim', explode( "\n", $value ) ) ); } else { $result[ $key ] = (array) $value; } } else { // String - preserve newlines for textarea fields if ( is_string( $value ) && ( strpos( $value, "\n" ) !== false || strpos( $value, "\r" ) !== false ) ) { $result[ $key ] = sanitize_textarea_field( $value ); } else { $result[ $key ] = sanitize_text_field( $value ); } } } } // Handle unchecked checkboxes: HTML forms don't submit unchecked boxes // If a boolean field exists in defaults but NOT in submitted_data, set it to false // // EXCEPTION: the top-level 'enabled' flag of every section is the // module's master switch and is controlled by the Dashboard module // toggle, NOT by a checkbox inside the section's form. Treating it // like a regular checkbox here would silently switch the module off // every time the user saves the tab — see the REST API enabled=false // regression. We only skip it at the top level (when section_name is // empty); nested 'enabled' fields like security_headers.csp.enabled // are real checkboxes and must keep the auto-unset behaviour. $preserve_top_level = array( 'enabled' ); foreach ( $defaults as $key => $default_value ) { if ( '' === $section_name && in_array( $key, $preserve_top_level, true ) ) { continue; } if ( is_bool( $default_value ) && ! array_key_exists( $key, $submitted_data ) ) { $result[ $key ] = false; } elseif ( is_array( $default_value ) && ! isset( $submitted_data[ $key ] ) ) { // Check if this is a flat value list (like excluded_users, enforced_roles, ip_whitelist) // vs a nested settings group (like rate_limiting, two_factor) // Flat lists: default is empty array OR all values are scalar $is_value_list = empty( $default_value ); if ( ! $is_value_list ) { $is_value_list = true; foreach ( $default_value as $dv ) { if ( ! is_scalar( $dv ) ) { $is_value_list = false; break; } } } if ( $is_value_list ) { // All items removed - reset to empty array $result[ $key ] = array(); } else { // Nested settings group - handle boolean children foreach ( $default_value as $nested_key => $nested_default ) { if ( is_bool( $nested_default ) && isset( $result[ $key ] ) && is_array( $result[ $key ] ) ) { $nested_submitted = isset( $submitted_data[ $key ] ) && is_array( $submitted_data[ $key ] ) ? $submitted_data[ $key ] : array(); if ( ! array_key_exists( $nested_key, $nested_submitted ) ) { $result[ $key ][ $nested_key ] = false; } } } } } } return $result; } /** * AJAX: Export settings */ public function ajax_export_settings() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } $options = $this->settings->get_all_options(); $filename = 'vigilante-settings-' . gmdate( 'Y-m-d-His' ) . '.json'; wp_send_json_success( array( 'content' => wp_json_encode( $options, JSON_PRETTY_PRINT ), 'filename' => $filename, ) ); } /** * AJAX: Import settings */ public function ajax_import_settings() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } // Accept both 'settings' (from JS) and 'content' (legacy). // Do NOT run sanitize_text_field() on the raw payload: it calls // wp_strip_all_tags() internally, which removes any "<...>" substring // and turns a valid export JSON into garbage if any stored value // contains < or > (htaccess snippets, email templates, etc.). The // real sanitization happens after json_decode(), via map_deep() on // the parsed array. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash $content = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : ''; if ( empty( $content ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash $content = isset( $_POST['content'] ) ? wp_unslash( $_POST['content'] ) : ''; } if ( empty( $content ) || ! is_string( $content ) ) { wp_send_json_error( __( 'No content provided.', 'vigilante' ) ); } $imported = json_decode( $content, true ); if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $imported ) ) { wp_send_json_error( __( 'Invalid JSON format.', 'vigilante' ) ); } // Sanitize imported data recursively $imported = map_deep( $imported, 'sanitize_text_field' ); // Validate structure $defaults = $this->settings->get_default_options(); $merged = array_replace_recursive( $defaults, $imported ); // Save update_option( Vigilante_Settings::OPTION_NAME, $merged ); $this->settings->clear_cache(); // Re-evaluate the active preset marker. The imported config may match // a known preset exactly, partially, or not at all — without this step // the dashboard would keep showing whatever preset was active before // the import even if the new config no longer matches it. $matched_preset = $this->detect_matching_preset( $merged ); if ( null === $matched_preset ) { delete_option( 'vigilante_active_preset' ); } else { update_option( 'vigilante_active_preset', $matched_preset ); } // Apply file changes after import $this->apply_all_file_changes( $merged ); // Refresh the Security Analyzer score so the dashboard widget reflects // the imported config rather than the pre-import scan. Reuse the // post-Under-Attack scan hook (same job: full scan, async). if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) { wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' ); } wp_send_json_success( __( 'Settings imported successfully.', 'vigilante' ) ); } /** * Detect whether a vigilante_options array matches a known preset. * * A preset matches when every field the preset explicitly declares is * present in the config with the same (normalised) value. Fields outside * the preset are ignored — they may have been modified by the user before * applying the preset and do not invalidate the match. This mirrors how * apply_preset() now layers presets on top of the user's existing config. * * @param array $options The current/imported vigilante_options. * @return string|null Preset id ('standard', 'maximum') or null if custom. */ private function detect_matching_preset( $options ) { if ( ! is_array( $options ) ) { return null; } $presets = $this->settings->get_presets(); foreach ( $presets as $preset_id => $preset_data ) { unset( $preset_data['name'], $preset_data['description'] ); if ( $this->preset_subset_matches( $preset_data, $options ) ) { return $preset_id; } } return null; } /** * Check whether every leaf value inside $preset_subset exists with the * same (normalised) value at the same path inside $config. * * @param mixed $preset_subset Branch of the preset definition. * @param mixed $config Same branch in the live/imported config. * @return bool */ private function preset_subset_matches( $preset_subset, $config ) { if ( is_array( $preset_subset ) ) { if ( ! is_array( $config ) ) { return false; } foreach ( $preset_subset as $key => $value ) { if ( ! array_key_exists( $key, $config ) ) { return false; } if ( ! $this->preset_subset_matches( $value, $config[ $key ] ) ) { return false; } } return true; } return $this->normalise_scalar_for_compare( $preset_subset ) === $this->normalise_scalar_for_compare( $config ); } /** * Normalise a scalar value so that the variants WordPress and the form * layer routinely produce ('1' / 1 / true → "1"; '' / '0' / 0 / false / * null → "") compare equal. Other values become strings unchanged. * * @param mixed $value * @return string */ private function normalise_scalar_for_compare( $value ) { if ( is_bool( $value ) ) { return $value ? '1' : ''; } if ( null === $value ) { return ''; } if ( is_int( $value ) || is_float( $value ) ) { return (string) $value; } if ( is_string( $value ) ) { if ( 'true' === $value ) { return '1'; } if ( 'false' === $value ) { return ''; } return $value; } // Arrays and objects shouldn't reach here (handled by recursion above), // but if they do, fall back to a stable comparable representation. return wp_json_encode( $value ); } /** * AJAX: Apply preset */ public function ajax_apply_preset() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } $preset = isset( $_POST['preset'] ) ? sanitize_key( $_POST['preset'] ) : ''; // Handle reset to defaults if ( 'reset' === $preset ) { $defaults = $this->settings->get_default_options(); update_option( Vigilante_Settings::OPTION_NAME, $defaults ); $this->settings->clear_cache(); // Clear active preset update_option( 'vigilante_active_preset', '' ); // Apply file changes after reset $this->apply_all_file_changes( $defaults ); wp_send_json_success( __( 'Settings reset to defaults.', 'vigilante' ) ); return; } $presets = $this->settings->get_presets(); if ( ! isset( $presets[ $preset ] ) ) { wp_send_json_error( __( 'Invalid preset.', 'vigilante' ) ); } $preset_options = $presets[ $preset ]; unset( $preset_options['name'], $preset_options['description'] ); // Layer the preset on top of the user's CURRENT configuration, not on // top of defaults. This way applying a preset only changes the fields // the preset explicitly mentions; everything else stays as the user // had it. For example, applying Maximum will not flip HSTS off if the // user had it on — Maximum doesn't touch HSTS, so it's left alone. // Use "Reset to Defaults" if a clean slate is needed. $current = get_option( Vigilante_Settings::OPTION_NAME, array() ); if ( ! is_array( $current ) ) { $current = array(); } // Make sure all known keys exist before merging — array_replace_recursive // does not invent keys that are missing on both sides. $current = array_replace_recursive( $this->settings->get_default_options(), $current ); $merged = array_replace_recursive( $current, $preset_options ); update_option( Vigilante_Settings::OPTION_NAME, $merged ); $this->settings->clear_cache(); // Save active preset update_option( 'vigilante_active_preset', $preset ); // Apply file changes after preset $this->apply_all_file_changes( $merged ); wp_send_json_success( __( 'Preset applied successfully.', 'vigilante' ) ); } /** * AJAX: Reset a specific section to defaults */ public function ajax_reset_section() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } $section = isset( $_POST['section'] ) ? sanitize_key( $_POST['section'] ) : ''; if ( empty( $section ) ) { wp_send_json_error( __( 'No section specified.', 'vigilante' ) ); } // Get current options and defaults $current_options = $this->settings->get_all_options(); $defaults = $this->settings->get_default_options(); // Check if section exists in defaults if ( ! isset( $defaults[ $section ] ) ) { wp_send_json_error( __( 'Invalid section.', 'vigilante' ) ); } // Reset only this section to defaults $current_options[ $section ] = $defaults[ $section ]; // Save update_option( Vigilante_Settings::OPTION_NAME, $current_options ); $this->settings->clear_cache(); // Apply file changes if needed $this->apply_section_changes( $section, $current_options ); wp_send_json_success( array( 'message' => __( 'Section reset to defaults.', 'vigilante' ), 'section' => $section, 'reload' => true, ) ); } /** * AJAX: Clear lockouts */ public function ajax_clear_lockouts() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } $ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : ''; if ( ! empty( $ip ) ) { $this->database->clear_lockout( $ip ); } else { $this->database->clear_all_lockouts(); } wp_send_json_success( __( 'Lockouts cleared.', 'vigilante' ) ); } /** * AJAX: Clear logs */ public function ajax_clear_logs() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } if ( $this->activity_log ) { $result = $this->activity_log->clear_all_logs(); if ( $result ) { wp_send_json_success( __( 'Logs cleared.', 'vigilante' ) ); } else { wp_send_json_error( __( 'Failed to clear logs.', 'vigilante' ) ); } } else { wp_send_json_error( __( 'Activity log not available.', 'vigilante' ) ); } } /** * AJAX: Run file integrity scan. * * Triggers the file integrity scan, which now also runs the closed plugins * check at the end when the `check_closed_plugins` toggle is on. Activity * log is passed through so Security Audit entries (both file-level and * plugin-status) are recorded from this entry point. */ public function ajax_run_scan() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } // Clear previous results before running new scan delete_option( 'vigilante_last_integrity_results' ); delete_option( 'vigilante_last_integrity_scan' ); $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log ); $results = $file_integrity->run_scan(); // Save new results update_option( 'vigilante_last_integrity_scan', time() ); update_option( 'vigilante_last_integrity_results', $results ); wp_send_json_success( array( 'message' => __( 'Scan completed.', 'vigilante' ), 'results' => $results, 'ignored_count' => count( get_option( 'vigilante_ignored_files', array() ) ), ) ); } /** * AJAX: Clear scan results. * * Wipes visually everything inside "Last Scan Results": file scan findings * (Suspicious / Modified / Extra / Critical Config), file hashes and the * Closed + Removed Plugins block. * * Implementation detail to keep persistence intact: * - We delete the file scan options + hashes outright. * - For plugin status we ONLY delete the last_check timestamp — the state * map and the ignore list are preserved in DB. The render gates the * plugin_status subsections on last_check > 0, so they hide after * Clear (visual reset) and reappear on the next Run Scan Now with the * state intact. This avoids degrading a 'removed' slug (404 without * metadata) back to 'not_in_repo' on the next scan, which would * silently lose the alert. * * The Ignored Files list and the Ignored Closed + Removed Plugins list * are preserved on purpose; each has its own explicit "Clear All …" * button. */ public function ajax_clear_scan() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } delete_option( 'vigilante_last_integrity_results' ); delete_option( 'vigilante_last_integrity_scan' ); if ( $this->database ) { $this->database->clear_file_hashes(); } // Visual reset of plugin_status block without touching the state map // or the ignored list. See PHPDoc above for the rationale. delete_option( 'vigilante_plugin_status_last_check' ); wp_send_json_success( __( 'Scan results cleared.', 'vigilante' ) ); } /** * AJAX: Ignore a file from scan results */ public function ajax_ignore_file() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } $file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : ''; if ( empty( $file ) ) { wp_send_json_error( __( 'No file specified.', 'vigilante' ) ); } $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database ); $file_integrity->ignore_file( $file ); // Also remove the file from stored scan results so UI updates $results = get_option( 'vigilante_last_integrity_results' ); if ( $results ) { foreach ( array( 'modified', 'suspicious', 'extra' ) as $category ) { if ( ! empty( $results[ $category ] ) ) { $results[ $category ] = array_values( array_filter( $results[ $category ], function ( $item ) use ( $file ) { return ( is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item ) !== $file; } ) ); } } update_option( 'vigilante_last_integrity_results', $results ); } wp_send_json_success( __( 'File added to ignored list.', 'vigilante' ) ); } /** * AJAX: Stop ignoring a file */ public function ajax_unignore_file() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } $file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : ''; if ( empty( $file ) ) { wp_send_json_error( __( 'No file specified.', 'vigilante' ) ); } $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database ); $file_integrity->unignore_file( $file ); wp_send_json_success( __( 'File removed from ignored list.', 'vigilante' ) ); } /** * AJAX: Bulk ignore multiple files at once * * Processes a single batch into ignored list and prunes them from the * stored scan results so the UI updates without a re-scan. */ public function ajax_bulk_ignore_files() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below per-item. $raw_files = isset( $_POST['files'] ) ? wp_unslash( $_POST['files'] ) : array(); if ( ! is_array( $raw_files ) ) { wp_send_json_error( __( 'Invalid request.', 'vigilante' ) ); } $files = array(); foreach ( $raw_files as $f ) { $clean = sanitize_text_field( $f ); if ( '' !== $clean ) { $files[] = $clean; } } if ( empty( $files ) ) { wp_send_json_error( __( 'No files selected.', 'vigilante' ) ); } $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database ); $count = 0; foreach ( $files as $file ) { $file_integrity->ignore_file( $file ); $count++; } // Also prune the stored scan results so the UI matches the new ignore list. $results = get_option( 'vigilante_last_integrity_results' ); if ( $results ) { $files_set = array_flip( $files ); foreach ( array( 'modified', 'suspicious', 'extra' ) as $category ) { if ( ! empty( $results[ $category ] ) ) { $results[ $category ] = array_values( array_filter( $results[ $category ], function ( $item ) use ( $files_set ) { $path = is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item; return ! isset( $files_set[ $path ] ); } ) ); } } update_option( 'vigilante_last_integrity_results', $results ); } wp_send_json_success( array( 'count' => $count, 'message' => sprintf( /* translators: %d: number of files added to the ignored list */ _n( '%d file added to ignored list.', '%d files added to ignored list.', $count, 'vigilante' ), $count ), ) ); } /** * AJAX: Bulk un-ignore multiple files at once */ public function ajax_bulk_unignore_files() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below per-item. $raw_files = isset( $_POST['files'] ) ? wp_unslash( $_POST['files'] ) : array(); if ( ! is_array( $raw_files ) ) { wp_send_json_error( __( 'Invalid request.', 'vigilante' ) ); } $files = array(); foreach ( $raw_files as $f ) { $clean = sanitize_text_field( $f ); if ( '' !== $clean ) { $files[] = $clean; } } if ( empty( $files ) ) { wp_send_json_error( __( 'No files selected.', 'vigilante' ) ); } $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database ); $count = 0; foreach ( $files as $file ) { $file_integrity->unignore_file( $file ); $count++; } wp_send_json_success( array( 'count' => $count, 'message' => sprintf( /* translators: %d: number of files removed from the ignored list */ _n( '%d file removed from ignored list.', '%d files removed from ignored list.', $count, 'vigilante' ), $count ), ) ); } /** * AJAX: Clear all ignored files */ public function ajax_clear_ignored() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database ); $file_integrity->clear_ignored_files(); wp_send_json_success( __( 'Ignored files list cleared.', 'vigilante' ) ); } /** * AJAX: Ignore a closed/removed plugin slug so it stops appearing in the * main list and email digests. The plugin keeps running on the site; * silencing is purely cosmetic and reversible. */ public function ajax_ignore_closed_plugin() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } $slug = isset( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : ''; if ( '' === $slug ) { wp_send_json_error( __( 'No slug specified.', 'vigilante' ) ); } if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) { require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php'; } $checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log ); $checker->ignore_slug( $slug ); wp_send_json_success( __( 'Plugin added to the ignored list.', 'vigilante' ) ); } /** * AJAX: Stop ignoring a previously-ignored closed/removed plugin slug. */ public function ajax_unignore_closed_plugin() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } $slug = isset( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : ''; if ( '' === $slug ) { wp_send_json_error( __( 'No slug specified.', 'vigilante' ) ); } if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) { require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php'; } $checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log ); $checker->unignore_slug( $slug ); wp_send_json_success( __( 'Plugin removed from the ignored list.', 'vigilante' ) ); } /** * AJAX: Clear the entire ignored-closed-plugins list. */ public function ajax_clear_ignored_closed_plugins() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) { require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php'; } $checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log ); $checker->clear_ignored(); wp_send_json_success( __( 'Ignored closed plugins list cleared.', 'vigilante' ) ); } /** * AJAX: Test security headers */ public function ajax_test_headers() { check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); } // Get security grade from settings (not from actual HTTP request) $security_headers = new Vigilante_Security_Headers( $this->settings ); $results = $security_headers->get_security_grade(); wp_send_json_success( $results ); } /** * Sanitize section data (kept for compatibility) * * @param string $section Section name. * @param array $data Section data. * @return array */ private function sanitize_section_data( $section, $data ) { $sanitized = array(); foreach ( $data as $key => $value ) { $key = sanitize_key( $key ); if ( is_array( $value ) ) { $sanitized[ $key ] = $this->sanitize_section_data( $key, $value ); } elseif ( is_numeric( $value ) ) { $sanitized[ $key ] = intval( $value ); } else { $sanitized[ $key ] = sanitize_text_field( $value ); } } return $sanitized; } /** * Apply changes after saving settings * * @param string $section Section that was updated. * @param array $all_options All options. */ private function apply_section_changes( $section, $all_options ) { // Create fresh settings instance to ensure we have the latest data $fresh_settings = new Vigilante_Settings(); // The shared Vigilant .htaccess block carries BOTH the firewall's // file-protection rules and the server-signature / fingerprinting rules // that are configured under Security Headers. So it must be regenerated // whenever either section (or the module toggles) changes, not only on // the firewall save — otherwise toggling "Hide server signature" or // "Remove fingerprinting headers" never reaches the .htaccess. if ( in_array( $section, array( 'firewall', 'security_headers', 'modules' ), true ) ) { $htaccess = new Vigilante_Htaccess_Protection( $fresh_settings ); $sh = $fresh_settings->get_section( 'security_headers' ); $needs_htaccess_block = ! empty( $all_options['modules']['firewall'] ) || ! empty( $sh['hide_server_signature'] ) || ! empty( $sh['remove_fingerprinting_headers'] ); if ( $needs_htaccess_block ) { $htaccess->apply_rules(); } else { $htaccess->remove_rules(); } } // Regenerate the HTTP security headers (sent by PHP) for the headers section. if ( 'security_headers' === $section || 'modules' === $section ) { $security_headers = new Vigilante_Security_Headers( $fresh_settings ); $headers_enabled = ! empty( $all_options['modules']['security_headers'] ); if ( $headers_enabled ) { $security_headers->apply_rules(); } else { $security_headers->remove_rules(); } } // Regenerate wp-config for wp_hardening section if ( 'wp_hardening' === $section || 'modules' === $section ) { $wpconfig = new Vigilante_Wpconfig_Security( $fresh_settings ); $hardening_enabled = ! empty( $all_options['modules']['wp_hardening'] ); if ( $hardening_enabled ) { $wpconfig->apply_security_constants(); } else { $wpconfig->remove_constants(); } // Apply WordPress options for comments/pingbacks $hardening_options = $all_options['wp_hardening'] ?? array(); if ( $hardening_enabled ) { // Pingbacks if ( ! empty( $hardening_options['disable_pingbacks'] ) ) { update_option( 'default_pingback_flag', 0 ); } else { // Restore default: pingbacks enabled update_option( 'default_pingback_flag', 1 ); } // Trackbacks and ping status // Only close if either pingbacks OR trackbacks are disabled if ( ! empty( $hardening_options['disable_pingbacks'] ) || ! empty( $hardening_options['disable_trackbacks'] ) ) { update_option( 'default_ping_status', 'closed' ); } else { // Restore default: pings open update_option( 'default_ping_status', 'open' ); } // Comment moderation if ( ! empty( $hardening_options['require_comment_moderation'] ) ) { update_option( 'comment_moderation', 1 ); } else { // Restore default: no moderation required update_option( 'comment_moderation', 0 ); } } } // Trim activity log entries immediately when limits change if ( 'activity_log' === $section && $this->activity_log ) { $this->activity_log->cleanup_old_logs(); } // Flush rewrite rules if login URL changed if ( 'login_security' === $section ) { $login_options = $all_options['login_security'] ?? array(); if ( ! empty( $login_options['custom_login_url'] ) ) { delete_option( 'vigilante_login_rules_version' ); } } // Log the settings change with readable section name if ( $this->activity_log ) { $section_names = array( 'firewall' => __( 'Firewall', 'vigilante' ), 'login_security' => __( 'Login Security', 'vigilante' ), 'security_headers' => __( 'Security Headers', 'vigilante' ), 'rest_api_security'=> __( 'REST API Security', 'vigilante' ), 'user_security' => __( 'User Security', 'vigilante' ), 'wp_hardening' => __( 'WP Hardening', 'vigilante' ), 'activity_log' => __( 'Security Audit', 'vigilante' ), 'file_integrity' => __( 'File Integrity', 'vigilante' ), 'email' => __( 'Notification Settings', 'vigilante' ), 'backup' => __( 'Backup', 'vigilante' ), 'advanced' => __( 'Advanced', 'vigilante' ), 'modules' => __( 'Modules', 'vigilante' ), ); $display_name = isset( $section_names[ $section ] ) ? $section_names[ $section ] : $section; $this->activity_log->log( 'settings', 'settings_updated', sprintf( /* translators: %s: Section name */ __( 'Settings updated: %s', 'vigilante' ), $display_name ), array( 'section' => $section ), 'info' ); } } /** * Apply all file changes (htaccess, wp-config) based on current options * * Used after preset, import, or reset operations * * @param array $all_options All plugin options. */ private function apply_all_file_changes( $all_options ) { // Refresh settings cache first $this->settings->clear_cache(); // Create fresh settings instance $fresh_settings = new Vigilante_Settings(); // Apply firewall htaccess changes $htaccess = new Vigilante_Htaccess_Protection( $fresh_settings ); $firewall_enabled = ! empty( $all_options['modules']['firewall'] ); if ( $firewall_enabled ) { $htaccess->apply_rules(); } else { $htaccess->remove_rules(); } // Apply security headers htaccess changes $security_headers = new Vigilante_Security_Headers( $fresh_settings ); $headers_enabled = ! empty( $all_options['modules']['security_headers'] ); if ( $headers_enabled ) { $security_headers->apply_rules(); } else { $security_headers->remove_rules(); } // Apply wp-config changes $wpconfig = new Vigilante_Wpconfig_Security( $fresh_settings ); $hardening_enabled = ! empty( $all_options['modules']['wp_hardening'] ); if ( $hardening_enabled ) { $wpconfig->apply_security_constants(); } else { $wpconfig->remove_constants(); } // Apply WordPress options for comments/pingbacks $hardening_options = $all_options['wp_hardening'] ?? array(); if ( $hardening_enabled ) { // Pingbacks if ( ! empty( $hardening_options['disable_pingbacks'] ) ) { update_option( 'default_pingback_flag', 0 ); } else { // Restore default: pingbacks enabled update_option( 'default_pingback_flag', 1 ); } // Trackbacks and ping status // Only close if either pingbacks OR trackbacks are disabled if ( ! empty( $hardening_options['disable_pingbacks'] ) || ! empty( $hardening_options['disable_trackbacks'] ) ) { update_option( 'default_ping_status', 'closed' ); } else { // Restore default: pings open update_option( 'default_ping_status', 'open' ); } // Comment moderation if ( ! empty( $hardening_options['require_comment_moderation'] ) ) { update_option( 'comment_moderation', 1 ); } else { // Restore default: no moderation required update_option( 'comment_moderation', 0 ); } } // Log the change if ( $this->activity_log ) { $this->activity_log->log( 'settings', 'bulk_settings_applied', __( 'Bulk settings applied (preset/import/reset)', 'vigilante' ), array(), 'info' ); } } }