PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.9.6
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.9.6
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
vigilante / admin / class-admin.php

class-admin.php in Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… 2.9.6, at admin/class-admin.php

7,351 lines 434.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin Class
4 *
5 * Handles admin interface and settings page
6 *
7 * @package Vigilante
8 */
9
10 // Prevent direct access
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 // Load AJAX trait
16 require_once VIGILANTE_ADMIN_DIR . 'class-admin-ajax.php';
17
18 // Load promotional banner class
19 require_once VIGILANTE_INCLUDES_DIR . 'class-ayudawp-promo-banner.php';
20
21 /**
22 * Class Vigilante_Admin
23 *
24 * Manages the admin settings interface
25 */
26 class Vigilante_Admin {
27
28 use Vigilante_Admin_Ajax;
29 use Vigilante_Admin_Analyzer_Ajax;
30 use Vigilante_Admin_Audit_Alerts_Ajax;
31
32 /**
33 * Settings instance
34 *
35 * @var Vigilante_Settings
36 */
37 private $settings;
38
39 /**
40 * Database instance
41 *
42 * @var Vigilante_Database
43 */
44 private $database;
45
46 /**
47 * Activity log instance
48 *
49 * @var Vigilante_Activity_Log
50 */
51 private $activity_log;
52
53 /**
54 * Current tab
55 *
56 * @var string
57 */
58 private $current_tab = 'dashboard';
59
60 /**
61 * Available tabs
62 *
63 * @var array
64 */
65 private $tabs = array();
66
67 /**
68 * Constructor
69 *
70 * @param Vigilante_Settings $settings Settings instance.
71 * @param Vigilante_Database $database Database instance.
72 * @param Vigilante_Activity_Log $activity_log Activity log instance.
73 */
74 public function __construct( $settings, $database, $activity_log ) {
75 $this->settings = $settings;
76 $this->database = $database;
77 $this->activity_log = $activity_log;
78
79 $this->setup_tabs();
80 $this->init_hooks();
81 }
82
83 /**
84 * Setup available tabs
85 */
86 private function setup_tabs() {
87 $this->tabs = array(
88 'dashboard' => __( 'Dashboard', 'vigilante' ),
89 'firewall' => __( 'Firewall', 'vigilante' ),
90 'headers' => __( 'Security Headers', 'vigilante' ),
91 'login' => __( 'Login Security', 'vigilante' ),
92 'rest-api' => __( 'REST API', 'vigilante' ),
93 'users' => __( 'User Security', 'vigilante' ),
94 'wp-hardening' => __( 'WP Hardening', 'vigilante' ),
95 'file-integrity' => __( 'File Integrity', 'vigilante' ),
96 'activity-log' => __( 'Security Audit', 'vigilante' ),
97 'tools' => __( 'Settings & Tools', 'vigilante' ),
98 );
99 }
100
101 /**
102 * Initialize hooks
103 */
104 private function init_hooks() {
105 add_action( 'admin_menu', array( $this, 'add_menu' ) );
106 add_action( 'admin_init', array( $this, 'redirect_submenu_shortcuts' ) );
107 add_action( 'admin_init', array( $this, 'register_settings' ) );
108 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
109 add_action( 'admin_notices', array( $this, 'show_admin_notices' ) );
110
111 // Highlight correct submenu based on active tab
112 add_filter( 'submenu_file', array( $this, 'highlight_submenu_tab' ) );
113
114 // Set browser tab title to show plugin name and active tab
115 add_filter( 'admin_title', array( $this, 'set_admin_page_title' ), 10, 2 );
116
117 // AJAX handlers
118 add_action( 'wp_ajax_vigilante_save_settings', array( $this, 'ajax_save_settings' ) );
119 add_action( 'wp_ajax_vigilante_apply_preset', array( $this, 'ajax_apply_preset' ) );
120 add_action( 'wp_ajax_vigilante_reset_section', array( $this, 'ajax_reset_section' ) );
121 add_action( 'wp_ajax_vigilante_clear_lockouts', array( $this, 'ajax_clear_lockouts' ) );
122 add_action( 'wp_ajax_vigilante_clear_logs', array( $this, 'ajax_clear_logs' ) );
123 add_action( 'wp_ajax_vigilante_run_scan', array( $this, 'ajax_run_scan' ) );
124 add_action( 'wp_ajax_vigilante_clear_scan', array( $this, 'ajax_clear_scan' ) );
125 add_action( 'wp_ajax_vigilante_ignore_file', array( $this, 'ajax_ignore_file' ) );
126 add_action( 'wp_ajax_vigilante_unignore_file', array( $this, 'ajax_unignore_file' ) );
127 add_action( 'wp_ajax_vigilante_bulk_ignore_files', array( $this, 'ajax_bulk_ignore_files' ) );
128 add_action( 'wp_ajax_vigilante_bulk_unignore_files', array( $this, 'ajax_bulk_unignore_files' ) );
129 add_action( 'wp_ajax_vigilante_clear_ignored', array( $this, 'ajax_clear_ignored' ) );
130 add_action( 'wp_ajax_vigilante_ignore_closed_plugin', array( $this, 'ajax_ignore_closed_plugin' ) );
131 add_action( 'wp_ajax_vigilante_unignore_closed_plugin', array( $this, 'ajax_unignore_closed_plugin' ) );
132 add_action( 'wp_ajax_vigilante_clear_ignored_closed_plugins', array( $this, 'ajax_clear_ignored_closed_plugins' ) );
133 add_action( 'wp_ajax_vigilante_approve_critical_file', array( $this, 'ajax_approve_critical_file' ) );
134 add_action( 'wp_ajax_vigilante_export_settings', array( $this, 'ajax_export_settings' ) );
135 add_action( 'wp_ajax_vigilante_import_settings', array( $this, 'ajax_import_settings' ) );
136 add_action( 'wp_ajax_vigilante_get_logs', array( $this, 'ajax_get_logs' ) );
137 add_action( 'wp_ajax_vigilante_test_headers', array( $this, 'ajax_test_headers' ) );
138 add_action( 'wp_ajax_vigilante_download_files_backup', array( $this, 'ajax_download_files_backup' ) );
139
140 // 2FA AJAX handlers
141 add_action( 'wp_ajax_vigilante_search_users_2fa', array( $this, 'ajax_search_users_2fa' ) );
142 add_action( 'wp_ajax_vigilante_send_2fa_notification', array( $this, 'ajax_send_2fa_notification' ) );
143 add_action( 'wp_ajax_vigilante_search_totp_users', array( $this, 'ajax_search_totp_users' ) );
144 add_action( 'wp_ajax_vigilante_reset_totp_users', array( $this, 'ajax_reset_totp_users' ) );
145 add_action( 'wp_ajax_vigilante_totp_get_setup', array( $this, 'ajax_totp_get_setup' ) );
146 add_action( 'wp_ajax_vigilante_notify_login_url', array( $this, 'ajax_notify_login_url' ) );
147
148 // Password Reset AJAX handlers
149 add_action( 'wp_ajax_vigilante_search_users_password_reset', array( $this, 'ajax_search_users_password_reset' ) );
150 add_action( 'wp_ajax_vigilante_force_password_reset', array( $this, 'ajax_force_password_reset' ) );
151 add_action( 'wp_ajax_vigilante_force_password_reset_all', array( $this, 'ajax_force_password_reset_all' ) );
152 add_action( 'wp_ajax_vigilante_force_password_reset_by_role', array( $this, 'ajax_force_password_reset_by_role' ) );
153
154 // User approval AJAX handlers
155 add_action( 'wp_ajax_vigilante_approve_user', array( $this, 'ajax_approve_user' ) );
156 add_action( 'wp_ajax_vigilante_reject_user', array( $this, 'ajax_reject_user' ) );
157
158 // Session management AJAX handlers
159 add_action( 'wp_ajax_vigilante_get_user_sessions', array( $this, 'ajax_get_user_sessions' ) );
160 add_action( 'wp_ajax_vigilante_revoke_session', array( $this, 'ajax_revoke_session' ) );
161 add_action( 'wp_ajax_vigilante_revoke_all_sessions', array( $this, 'ajax_revoke_all_sessions' ) );
162
163 // Under Attack mode AJAX handlers
164 add_action( 'wp_ajax_vigilante_activate_under_attack', array( $this, 'ajax_activate_under_attack' ) );
165 add_action( 'wp_ajax_vigilante_deactivate_under_attack', array( $this, 'ajax_deactivate_under_attack' ) );
166 add_action( 'wp_ajax_vigilante_under_attack_status', array( $this, 'ajax_under_attack_status' ) );
167
168 // Database backup AJAX handlers
169 add_action( 'wp_ajax_vigilante_get_db_tables', array( $this, 'ajax_get_db_tables' ) );
170 add_action( 'wp_ajax_vigilante_download_db_backup', array( $this, 'ajax_download_db_backup' ) );
171
172 // Database prefix AJAX handlers
173 add_action( 'wp_ajax_vigilante_generate_prefix', array( $this, 'ajax_generate_prefix' ) );
174 add_action( 'wp_ajax_vigilante_change_prefix', array( $this, 'ajax_change_prefix' ) );
175
176 // Firewall list management from activity log popup
177 add_action( 'wp_ajax_vigilante_add_to_firewall_list', array( $this, 'ajax_add_to_firewall_list' ) );
178 add_action( 'wp_ajax_vigilante_unblock_firewall_ip', array( $this, 'ajax_unblock_firewall_ip' ) );
179
180 // Security Analyzer AJAX handlers (v2.1.0)
181 add_action( 'wp_ajax_vigilante_analyzer_run', array( $this, 'ajax_analyzer_run' ) );
182 add_action( 'wp_ajax_vigilante_analyzer_history', array( $this, 'ajax_analyzer_history' ) );
183 add_action( 'wp_ajax_vigilante_analyzer_dismiss_notice', array( $this, 'ajax_analyzer_dismiss_notice' ) );
184 add_action( 'wp_ajax_vigilante_analyzer_save_settings', array( $this, 'ajax_analyzer_save_settings' ) );
185
186 // Shared "Send test email" handler — Notification settings, File Integrity, Audit Alerts (v2.8.0)
187 add_action( 'wp_ajax_vigilante_send_test_email', array( $this, 'ajax_send_test_email' ) );
188
189 // Run migrations on admin load
190 add_action( 'admin_init', array( $this, 'run_migrations' ) );
191 }
192
193 /**
194 * Run database migrations based on stored version
195 */
196 public function run_migrations() {
197 $db_version = get_option( 'vigilante_db_version', '0' );
198
199 // 1.2.3: Fix IP lists corrupted by sanitize_text_field stripping newlines
200 if ( version_compare( $db_version, '1.2.3', '<' ) ) {
201 $this->migrate_fix_ip_lists();
202 update_option( 'vigilante_db_version', '1.2.3' );
203 }
204
205 // 1.3.0: Add request_method column to activity log table
206 if ( version_compare( $db_version, '1.3.0', '<' ) ) {
207 $this->database->run_migrations();
208 }
209
210 // 1.9.0: Re-apply wp-config constants (performance constants removed from managed list)
211 if ( version_compare( $db_version, '1.9.0', '<' ) ) {
212 if ( $this->settings->is_module_enabled( 'wp_hardening' ) ) {
213 require_once VIGILANTE_INCLUDES_DIR . 'class-wpconfig-security.php';
214 $wpconfig = new Vigilante_Wpconfig_Security( $this->settings );
215 $wpconfig->apply_security_constants();
216 }
217 update_option( 'vigilante_db_version', '1.9.0' );
218 }
219
220 // 1.10.0: Clean up orphaned email fields (centralized notification recipients)
221 if ( version_compare( $db_version, '1.10.0', '<' ) ) {
222 $this->migrate_cleanup_email_fields();
223 update_option( 'vigilante_db_version', '1.10.0' );
224 }
225
226 // 1.11.0: Remove stale 'enabled' key from activity_log settings
227 // + Convert additional_recipients from string to array
228 if ( version_compare( $db_version, '1.11.0', '<' ) ) {
229 $options = get_option( Vigilante_Settings::OPTION_NAME, array() );
230 $changed = false;
231
232 if ( isset( $options['activity_log']['enabled'] ) ) {
233 unset( $options['activity_log']['enabled'] );
234 $changed = true;
235 }
236
237 // Convert corrupted string to array for additional_recipients
238 if ( isset( $options['email']['additional_recipients'] ) && is_string( $options['email']['additional_recipients'] ) ) {
239 $raw = trim( $options['email']['additional_recipients'] );
240 if ( ! empty( $raw ) ) {
241 $emails = array_filter( array_map( 'trim', preg_split( '/[\r\n,; ]+/', $raw ) ) );
242 $options['email']['additional_recipients'] = array_values( array_filter( $emails, 'is_email' ) );
243 } else {
244 $options['email']['additional_recipients'] = array();
245 }
246 $changed = true;
247 }
248
249 if ( $changed ) {
250 update_option( Vigilante_Settings::OPTION_NAME, $options );
251 }
252 update_option( 'vigilante_db_version', '1.11.0' );
253 }
254
255 // 1.12.1: Regenerate htaccess (WooCommerce IPN exclusion in bot blocking rule)
256 if ( version_compare( $db_version, '1.12.1', '<' ) ) {
257 if ( ! empty( $this->settings->get_section( 'firewall' )['block_bad_bots'] ) ) {
258 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php';
259 $htaccess = new Vigilante_Htaccess_Protection( $this->settings );
260 $htaccess->apply_rules();
261 }
262 update_option( 'vigilante_db_version', '1.12.1' );
263 }
264
265 // 1.14.0: Generate critical config files baseline (wp-config.php, .htaccess)
266 if ( version_compare( $db_version, '1.14.0', '<' ) ) {
267 if ( ! class_exists( 'Vigilante_File_Integrity' ) ) {
268 require_once VIGILANTE_INCLUDES_DIR . 'class-file-integrity.php';
269 }
270 $fi = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log );
271 $fi->regenerate_all_baselines();
272 update_option( 'vigilante_db_version', '1.14.0' );
273 }
274
275 // 2.0.0: Move hide_server_signature and remove_fingerprinting_headers
276 // from firewall section to security_headers section
277 if ( version_compare( $db_version, '2.0.0', '<' ) ) {
278 $options = get_option( Vigilante_Settings::OPTION_NAME, array() );
279 $changed = false;
280
281 foreach ( array( 'hide_server_signature', 'remove_fingerprinting_headers' ) as $key ) {
282 if ( isset( $options['firewall'][ $key ] ) ) {
283 if ( ! isset( $options['security_headers'][ $key ] ) ) {
284 $options['security_headers'][ $key ] = $options['firewall'][ $key ];
285 }
286 unset( $options['firewall'][ $key ] );
287 $changed = true;
288 }
289 }
290
291 if ( $changed ) {
292 update_option( Vigilante_Settings::OPTION_NAME, $options );
293 }
294 update_option( 'vigilante_db_version', '2.0.0' );
295 }
296
297 // 2.6.1: Two things happen here.
298 //
299 // 1. Re-apply wp-config constants so the block is rewritten with
300 // "if ( ! defined() )" guards around every define(). Without guards,
301 // non-standard setups that pre-define WordPress constants outside
302 // wp-config.php (custom bootstraps that load constants from .env or
303 // similar) hit a fatal "Constant already defined" when wp-config.php
304 // is parsed and reaches our block.
305 //
306 // 2. Drop the cached Security Check report. The cached "max" per
307 // category was frozen at scan time; with the internal category
308 // bumping from 22 to 28 points (closed_plugins added in 2.6.0),
309 // the cached report would keep displaying 22/22 until the next
310 // full scan. Clearing it forces a fresh scan with the new caps.
311 if ( version_compare( $db_version, '2.6.1', '<' ) ) {
312 if ( $this->settings->is_module_enabled( 'wp_hardening' ) ) {
313 require_once VIGILANTE_INCLUDES_DIR . 'class-wpconfig-security.php';
314 $wpconfig = new Vigilante_Wpconfig_Security( $this->settings );
315 $wpconfig->apply_security_constants();
316 }
317 delete_option( 'vigilante_analyzer_last_scan' );
318
319 // Schedule an immediate background scan so the dashboard widget
320 // doesn't display "Last scan: never" right after the upgrade.
321 // Reuses the same hook the post-Under-Attack flow uses.
322 if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) {
323 wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' );
324 }
325
326 update_option( 'vigilante_db_version', '2.6.1' );
327 }
328
329 // 2.9.3: Regenerate the .htaccess protection block. The bad-bots
330 // User-Agent list dropped substring-prone tokens that 403'd
331 // legitimate clients (e.g. "rma" matched inside "Performance" and
332 // blocked WP Rocket's page fetch), and the blocking rules now honour
333 // the firewall IP / User-Agent whitelists as negated exceptions.
334 // Existing sites only rewrite the block when Server Protection is
335 // saved, so the upgrade has to refresh it once itself (same pattern
336 // as the 1.12.1 WooCommerce IPN migration).
337 if ( version_compare( $db_version, '2.9.3', '<' ) ) {
338 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php';
339 $htaccess = new Vigilante_Htaccess_Protection( $this->settings );
340
341 if ( $htaccess->are_rules_active() ) {
342 $htaccess->apply_rules();
343 }
344
345 update_option( 'vigilante_db_version', '2.9.3' );
346 }
347 }
348
349 /**
350 * Migration: Remove orphaned email fields from saved options
351 *
352 * v1.10.0 centralized notification recipients into email section.
353 * Old per-module notify_email fields and dead email section fields
354 * are removed to avoid confusion.
355 */
356 private function migrate_cleanup_email_fields() {
357 $options = get_option( Vigilante_Settings::OPTION_NAME, array() );
358 $modified = false;
359
360 // Remove orphaned fields from email section
361 $dead_email_keys = array( 'enabled', 'from_name', 'from_email', 'admin_email', 'send_activation_email', 'custom_email' );
362 if ( isset( $options['email'] ) && is_array( $options['email'] ) ) {
363 foreach ( $dead_email_keys as $key ) {
364 if ( array_key_exists( $key, $options['email'] ) ) {
365 unset( $options['email'][ $key ] );
366 $modified = true;
367 }
368 }
369 // Ensure new fields exist with defaults
370 if ( ! array_key_exists( 'send_to_admin_email', $options['email'] ) ) {
371 $options['email']['send_to_admin_email'] = true;
372 $modified = true;
373 }
374 if ( ! array_key_exists( 'additional_recipients', $options['email'] ) ) {
375 $options['email']['additional_recipients'] = '';
376 $modified = true;
377 }
378 }
379
380 // Remove notify_email from login_security
381 if ( isset( $options['login_security']['notify_email'] ) ) {
382 unset( $options['login_security']['notify_email'] );
383 $modified = true;
384 }
385
386 // Remove notify_email from file_integrity
387 if ( isset( $options['file_integrity']['notify_email'] ) ) {
388 unset( $options['file_integrity']['notify_email'] );
389 $modified = true;
390 }
391
392 if ( $modified ) {
393 update_option( Vigilante_Settings::OPTION_NAME, $options );
394 // Clear settings cache so the plugin uses clean data immediately
395 $this->settings->clear_cache();
396 }
397 }
398
399 /**
400 * Migration: Fix IP whitelist/blacklist entries merged into single line
401 *
402 * Prior to 1.2.3, sanitize_text_field() stripped newlines from textarea data,
403 * causing multiple IPs to be stored as a single space-separated string.
404 */
405 private function migrate_fix_ip_lists() {
406 $options = get_option( Vigilante_Settings::OPTION_NAME, array() );
407 $fixed = false;
408
409 foreach ( array( 'ip_whitelist', 'ip_blacklist' ) as $key ) {
410 if ( ! empty( $options['firewall'][ $key ] ) && is_array( $options['firewall'][ $key ] ) ) {
411 $new_list = array();
412 foreach ( $options['firewall'][ $key ] as $entry ) {
413 // Split entries that were joined by spaces
414 $parts = preg_split( '/\s+/', trim( $entry ) );
415 foreach ( $parts as $part ) {
416 $part = trim( $part );
417 if ( '' !== $part ) {
418 $new_list[] = $part;
419 }
420 }
421 }
422 if ( count( $new_list ) !== count( $options['firewall'][ $key ] ) ) {
423 $options['firewall'][ $key ] = array_unique( $new_list );
424 $fixed = true;
425 }
426 }
427 }
428
429 if ( $fixed ) {
430 update_option( Vigilante_Settings::OPTION_NAME, $options );
431 // Clear settings cache so changes take effect immediately
432 $this->settings->clear_cache();
433 }
434 }
435
436 /**
437 * Add admin menu page in last position
438 */
439 public function add_menu() {
440 $menu_title = __( 'Vigilant', 'vigilante' );
441
442 // Count pending approvals (separate concern, always red if present)
443 $pending_count = $this->get_pending_approvals_count();
444
445 // Get security issues with severity
446 $security_status = $this->get_security_status_for_badge();
447
448 // Total count for badge
449 $total_badge = $pending_count + $security_status['count'];
450
451 if ( $total_badge > 0 ) {
452 // Determine badge color:
453 // - Red (awaiting-mod): pending approvals OR critical modules disabled
454 // - Orange (update-plugins): only non-critical modules disabled
455 if ( $pending_count > 0 || $security_status['has_critical'] ) {
456 $badge_class = 'awaiting-mod';
457 } else {
458 $badge_class = 'update-plugins vigilante-badge-warning';
459 }
460
461 $menu_title .= sprintf(
462 ' <span class="%s count-%d"><span class="pending-count">%d</span></span>',
463 esc_attr( $badge_class ),
464 $total_badge,
465 $total_badge
466 );
467 }
468
469 add_menu_page(
470 __( 'Vigilant', 'vigilante' ),
471 $menu_title,
472 'manage_options',
473 'vigilante',
474 array( $this, 'render_settings_page' ),
475 'dashicons-shield',
476 999
477 );
478
479 // Rename auto-generated first submenu to "Dashboard"
480 add_submenu_page(
481 'vigilante',
482 __( 'Dashboard', 'vigilante' ),
483 __( 'Dashboard', 'vigilante' ),
484 'manage_options',
485 'vigilante',
486 array( $this, 'render_settings_page' )
487 );
488
489 // Security Audit shortcut
490 add_submenu_page(
491 'vigilante',
492 __( 'Security Audit', 'vigilante' ),
493 __( 'Security Audit', 'vigilante' ),
494 'manage_options',
495 'vigilante-activity-log',
496 array( $this, 'redirect_to_tab' )
497 );
498
499 // File Integrity shortcut
500 add_submenu_page(
501 'vigilante',
502 __( 'File Integrity', 'vigilante' ),
503 __( 'File Integrity', 'vigilante' ),
504 'manage_options',
505 'vigilante-file-integrity',
506 array( $this, 'redirect_to_tab' )
507 );
508 }
509
510 /**
511 * Redirect submenu shortcuts early, before headers are sent
512 */
513 public function redirect_submenu_shortcuts() {
514 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Just reading page slug for redirect
515 $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
516
517 $tab_map = array(
518 'vigilante-activity-log' => 'activity-log',
519 'vigilante-file-integrity' => 'file-integrity',
520 );
521
522 if ( isset( $tab_map[ $page ] ) ) {
523 wp_safe_redirect( admin_url( 'admin.php?page=vigilante&tab=' . $tab_map[ $page ] ) );
524 exit;
525 }
526 }
527
528 /**
529 * Fallback redirect for submenu shortcuts (JS-based)
530 */
531 public function redirect_to_tab() {
532 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Just reading page slug for redirect
533 $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
534
535 $tab_map = array(
536 'vigilante-activity-log' => 'activity-log',
537 'vigilante-file-integrity' => 'file-integrity',
538 );
539
540 if ( isset( $tab_map[ $page ] ) ) {
541 $url = admin_url( 'admin.php?page=vigilante&tab=' . $tab_map[ $page ] );
542 echo '<script>window.location.replace(' . wp_json_encode( esc_url( $url ) ) . ');</script>';
543 }
544 }
545
546 /**
547 * Highlight the correct submenu item based on active tab
548 *
549 * @param string $submenu_file Current submenu file.
550 * @return string
551 */
552 public function highlight_submenu_tab( $submenu_file ) {
553 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading tab for menu highlight only
554 $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
555 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
556 $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : '';
557
558 if ( 'vigilante' !== $page || empty( $tab ) ) {
559 return $submenu_file;
560 }
561
562 $tab_to_submenu = array(
563 'activity-log' => 'vigilante-activity-log',
564 'file-integrity' => 'vigilante-file-integrity',
565 );
566
567 if ( isset( $tab_to_submenu[ $tab ] ) ) {
568 return $tab_to_submenu[ $tab ];
569 }
570
571 return $submenu_file;
572 }
573
574 /**
575 * Set browser tab title to show plugin name and active tab
576 *
577 * Changes "Dashboard ‹ Site Name — WordPress" to
578 * "Vigilant > Dashboard ‹ Site Name — WordPress"
579 *
580 * @param string $admin_title Full admin title.
581 * @param string $title Page title from add_menu_page/add_submenu_page.
582 * @return string Modified title.
583 */
584 public function set_admin_page_title( $admin_title, $title ) {
585 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading page slug for title only
586 $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
587
588 // Only modify on Vigilante pages
589 if ( 0 !== strpos( $page, 'vigilante' ) ) {
590 return $admin_title;
591 }
592
593 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
594 $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'dashboard';
595
596 if ( isset( $this->tabs[ $tab ] ) ) {
597 $tab_label = $this->tabs[ $tab ];
598 } else {
599 $tab_label = __( 'Dashboard', 'vigilante' );
600 }
601
602 $plugin_title = __( 'Vigilant', 'vigilante' ) . ' &rsaquo; ' . $tab_label;
603
604 // Replace the original page title portion
605 return str_replace( $title, $plugin_title, $admin_title );
606 }
607
608 /**
609 * Get count of users pending approval
610 *
611 * @return int Count of pending users.
612 */
613 private function get_pending_approvals_count() {
614 // Prevent early execution before WordPress is ready
615 if ( ! did_action( 'plugins_loaded' ) ) {
616 return 0;
617 }
618
619 $registration_approval = $this->settings->get_section( 'user_security' );
620 $approval_settings = $registration_approval['registration_approval'] ?? array();
621
622 if ( empty( $approval_settings['enabled'] ) ) {
623 return 0;
624 }
625
626 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Limited results in admin context.
627 $pending_users = get_users( array(
628 'meta_key' => 'vigilante_pending_approval',
629 'meta_value' => '1',
630 'fields' => 'ID',
631 ) );
632 // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
633
634 return count( $pending_users );
635 }
636
637 /**
638 * Calculate comprehensive security score (0-100)
639 *
640 * @param array $options Plugin options.
641 * @return int Security score.
642 */
643 private function calculate_security_score( $options ) {
644 $score = 0;
645 $max_score = 0;
646
647 // Module scores (60 points total)
648 $module_weights = array(
649 'firewall' => 10,
650 'security_headers' => 8,
651 'login_security' => 10,
652 'rest_api_security' => 6,
653 'user_security' => 8,
654 'wp_hardening' => 8,
655 'file_integrity' => 5,
656 'activity_log' => 5,
657 );
658
659 foreach ( $module_weights as $module => $weight ) {
660 $max_score += $weight;
661 if ( ! empty( $options['modules'][ $module ] ) ) {
662 $score += $weight;
663 }
664 }
665
666 // Firewall details (10 points)
667 if ( ! empty( $options['modules']['firewall'] ) ) {
668 $firewall = $options['firewall'] ?? array();
669 $max_score += 10;
670
671 $firewall_checks = array(
672 'block_sql_injection',
673 'block_xss_attacks',
674 'block_bad_query_strings',
675 'block_file_inclusion',
676 'block_directory_traversal',
677 );
678
679 $firewall_enabled = 0;
680 foreach ( $firewall_checks as $check ) {
681 if ( ! empty( $firewall[ $check ] ) ) {
682 $firewall_enabled++;
683 }
684 }
685 $score += min( 10, $firewall_enabled * 2 );
686 }
687
688 // Login security details (10 points)
689 if ( ! empty( $options['modules']['login_security'] ) ) {
690 $login = $options['login_security'] ?? array();
691 $max_score += 10;
692
693 // Max attempts configured
694 if ( isset( $login['max_attempts'] ) && $login['max_attempts'] <= 5 ) {
695 $score += 3;
696 }
697 // XML-RPC disabled
698 if ( ! empty( $login['disable_xmlrpc'] ) ) {
699 $score += 3;
700 }
701 // 2FA enabled
702 if ( ! empty( $login['two_factor']['enabled'] ) ) {
703 $score += 4;
704 }
705 }
706
707 // Security headers details (10 points)
708 if ( ! empty( $options['modules']['security_headers'] ) ) {
709 $headers = $options['security_headers'] ?? array();
710 $max_score += 10;
711
712 if ( ! empty( $headers['x_frame_options'] ) ) {
713 $score += 2;
714 }
715 if ( ! empty( $headers['x_content_type_options'] ) ) {
716 $score += 2;
717 }
718 if ( ! empty( $headers['hsts']['enabled'] ) ) {
719 $score += 3;
720 }
721 if ( ! empty( $headers['csp']['enabled'] ) ) {
722 $score += 3;
723 }
724 }
725
726 // User security details (10 points)
727 if ( ! empty( $options['modules']['user_security'] ) ) {
728 $user = $options['user_security'] ?? array();
729 $max_score += 10;
730
731 if ( ! empty( $user['block_insecure_usernames'] ) ) {
732 $score += 3;
733 }
734 if ( ! empty( $user['force_strong_passwords'] ) ) {
735 $score += 3;
736 }
737 if ( ! empty( $user['password_expiration']['enabled'] ) ) {
738 $score += 2;
739 }
740 if ( ! empty( $user['email_verification']['enabled'] ) ) {
741 $score += 2;
742 }
743 }
744
745 // Audit alerts details (6 points) - only when Security Audit is on,
746 // because the alerting layer rides on top of the activity log. Leaving
747 // both alert legs off keeps these points unearned.
748 if ( ! empty( $options['modules']['activity_log'] ) ) {
749 $alerts = isset( $options['audit_alerts'] ) ? (array) $options['audit_alerts'] : array();
750 $max_score += 6;
751 if ( Vigilante_Audit_Alerts::immediate_is_active( $alerts ) ) {
752 $score += 3;
753 }
754 if ( Vigilante_Audit_Alerts::threshold_is_active( $alerts ) ) {
755 $score += 3;
756 }
757 }
758
759 // Environment checks (8 points) - penalize insecure server configuration
760 $max_score += 8;
761 $env_score = 8;
762
763 // WP_DEBUG active in production is a security risk (exposes paths, errors)
764 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
765 $env_score -= 5;
766 }
767
768 // Accounts with insecure usernames (targeted by brute force attacks)
769 $insecure_admins = $this->get_insecure_admin_usernames();
770 if ( ! empty( $insecure_admins ) ) {
771 $env_score -= 3;
772 }
773
774 $score += max( 0, $env_score );
775
776 return $max_score > 0 ? round( ( $score / $max_score ) * 100 ) : 0;
777 }
778
779 /**
780 * Get security recommendations based on current settings
781 *
782 * @param array $options Plugin options.
783 * @return array Array of recommendations.
784 */
785 private function get_security_recommendations( $options ) {
786 $recommendations = array();
787
788 // Critical: Firewall disabled
789 if ( empty( $options['modules']['firewall'] ) ) {
790 $recommendations[] = array(
791 'icon' => 'warning',
792 'priority' => 'critical',
793 'message' => __( 'Enable Firewall to protect against common attacks.', 'vigilante' ),
794 );
795 }
796
797 // Critical: Login security disabled
798 if ( empty( $options['modules']['login_security'] ) ) {
799 $recommendations[] = array(
800 'icon' => 'warning',
801 'priority' => 'critical',
802 'message' => __( 'Enable Login Security to prevent brute force attacks.', 'vigilante' ),
803 );
804 }
805
806 // High: Security headers disabled
807 if ( empty( $options['modules']['security_headers'] ) ) {
808 $recommendations[] = array(
809 'icon' => 'admin-generic',
810 'priority' => 'high',
811 'message' => __( 'Enable Security Headers to protect against clickjacking and XSS.', 'vigilante' ),
812 );
813 }
814
815 // High: User security disabled
816 if ( empty( $options['modules']['user_security'] ) ) {
817 $recommendations[] = array(
818 'icon' => 'admin-users',
819 'priority' => 'high',
820 'message' => __( 'Enable User Security to enforce password policies and username protection.', 'vigilante' ),
821 );
822 }
823
824 // High: 2FA not enabled (only if login security is active)
825 $login = $options['login_security'] ?? array();
826 if ( ! empty( $options['modules']['login_security'] ) && empty( $login['two_factor']['enabled'] ) ) {
827 $recommendations[] = array(
828 'icon' => 'shield',
829 'priority' => 'high',
830 'message' => __( 'Enable Two-Factor Authentication for enhanced login security.', 'vigilante' ),
831 'tab' => 'login',
832 );
833 }
834
835 // Medium: REST API security disabled
836 if ( empty( $options['modules']['rest_api_security'] ) ) {
837 $recommendations[] = array(
838 'icon' => 'rest-api',
839 'priority' => 'medium',
840 'message' => __( 'Enable REST API Security to control API access and prevent enumeration.', 'vigilante' ),
841 );
842 }
843
844 // Medium: WP Hardening disabled
845 if ( empty( $options['modules']['wp_hardening'] ) ) {
846 $recommendations[] = array(
847 'icon' => 'lock',
848 'priority' => 'medium',
849 'message' => __( 'Enable WP Hardening to remove version info and protect core files.', 'vigilante' ),
850 );
851 }
852
853 // Medium: File integrity disabled
854 if ( empty( $options['modules']['file_integrity'] ) ) {
855 $recommendations[] = array(
856 'icon' => 'media-text',
857 'priority' => 'medium',
858 'message' => __( 'Enable File Integrity to detect unauthorized file changes.', 'vigilante' ),
859 );
860 }
861
862 // Medium: Security Audit disabled
863 if ( empty( $options['modules']['activity_log'] ) ) {
864 $recommendations[] = array(
865 'icon' => 'list-view',
866 'priority' => 'medium',
867 'message' => __( 'Enable Security Audit to track security events.', 'vigilante' ),
868 );
869 }
870
871 // Low: XML-RPC enabled (only if login security is active)
872 if ( ! empty( $options['modules']['login_security'] ) && empty( $login['disable_xmlrpc'] ) ) {
873 $recommendations[] = array(
874 'icon' => 'info',
875 'priority' => 'low',
876 'message' => __( 'Disable XML-RPC if not needed (reduces attack surface).', 'vigilante' ),
877 'tab' => 'login',
878 );
879 }
880
881 // Low: Strong passwords not enforced (only if user security is active)
882 $user = $options['user_security'] ?? array();
883 if ( ! empty( $options['modules']['user_security'] ) && empty( $user['force_strong_passwords'] ) ) {
884 $recommendations[] = array(
885 'icon' => 'admin-users',
886 'priority' => 'low',
887 'message' => __( 'Enforce strong passwords for all users.', 'vigilante' ),
888 'tab' => 'users',
889 );
890 }
891
892 // High: WP_DEBUG active in production (regardless of Vigilante settings)
893 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
894 $recommendations[] = array(
895 'icon' => 'warning',
896 'priority' => 'high',
897 'message' => __( 'WP_DEBUG is active. Debug mode exposes sensitive information and should be disabled in production.', 'vigilante' ),
898 'tab' => 'wp-hardening',
899 );
900 }
901
902 // Low: Users with display name matching login username (only if user security active)
903 if ( ! empty( $options['modules']['user_security'] ) ) {
904 $exposed_users = $this->get_users_with_exposed_login();
905 if ( ! empty( $exposed_users ) ) {
906 $recommendations[] = array(
907 'icon' => 'admin-users',
908 'priority' => 'low',
909 'message' => sprintf(
910 /* translators: %s: Comma-separated list of usernames */
911 __( 'These users have their login username as display name (publicly visible): %s', 'vigilante' ),
912 implode( ', ', $exposed_users )
913 ),
914 'tab' => 'users',
915 );
916 }
917 }
918
919 // High: Accounts with insecure usernames (regardless of module status)
920 $insecure_admins = $this->get_insecure_admin_usernames();
921 if ( ! empty( $insecure_admins ) ) {
922 $recommendations[] = array(
923 'icon' => 'warning',
924 'priority' => 'high',
925 'message' => sprintf(
926 /* translators: %s: Comma-separated list of usernames */
927 __( 'Insecure usernames detected: %s. These are commonly targeted in brute force attacks. Create new accounts with unique usernames and remove these.', 'vigilante' ),
928 implode( ', ', $insecure_admins )
929 ),
930 );
931 }
932
933 // Critical: Closed or removed plugins detected by the daily check.
934 // Reads from the cached state map populated by Vigilante_Plugin_Status, so
935 // there is no extra HTTP call here. Ignored slugs are filtered out so the
936 // recommendation respects the user's per-slug Ignore decisions.
937 if ( ! empty( $options['modules']['file_integrity'] ) && ! empty( $options['file_integrity']['check_closed_plugins'] ) ) {
938 if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) {
939 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
940 }
941 $closed_checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
942 $closed_active = $closed_checker->get_closed_plugins();
943 if ( ! empty( $closed_active ) ) {
944 $names = array();
945 foreach ( $closed_active as $slug => $entry ) {
946 $names[] = isset( $entry['name'] ) ? $entry['name'] : $slug;
947 }
948 $recommendations[] = array(
949 'icon' => 'warning',
950 'priority' => 'critical',
951 'message' => sprintf(
952 /* translators: 1: count, 2: comma-separated plugin names */
953 _n(
954 '%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.',
955 '%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.',
956 count( $closed_active ),
957 'vigilante'
958 ),
959 count( $closed_active ),
960 implode( ', ', $names )
961 ),
962 'tab' => 'file-integrity',
963 );
964 }
965 }
966
967 // Medium: Security Audit is on but no audit alert is configured. The
968 // alerting layer only makes sense while the activity log is running.
969 if ( ! empty( $options['modules']['activity_log'] ) ) {
970 $alerts = isset( $options['audit_alerts'] ) ? (array) $options['audit_alerts'] : array();
971 if ( ! Vigilante_Audit_Alerts::has_active_alerts( $alerts ) ) {
972 $recommendations[] = array(
973 'icon' => 'email-alt',
974 'priority' => 'medium',
975 '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' ),
976 'tab' => 'activity-log',
977 );
978 }
979 }
980
981 // Sort by priority
982 $priority_order = array( 'critical' => 0, 'high' => 1, 'medium' => 2, 'low' => 3 );
983 usort( $recommendations, function( $a, $b ) use ( $priority_order ) {
984 return ( $priority_order[ $a['priority'] ] ?? 99 ) - ( $priority_order[ $b['priority'] ] ?? 99 );
985 } );
986
987 return $recommendations;
988 }
989
990 /**
991 * Get users whose display name matches their login username
992 *
993 * Limited to administrators and editors for performance and relevance.
994 * Cached with transient to avoid repeated queries on every dashboard load.
995 *
996 * @return array Array of usernames with exposed login.
997 */
998 private function get_users_with_exposed_login() {
999 $cache_key = 'vigilante_exposed_display_names';
1000 $cached = get_transient( $cache_key );
1001
1002 if ( false !== $cached ) {
1003 return $cached;
1004 }
1005
1006 $exposed = array();
1007 $users = get_users( array(
1008 'role__in' => array( 'administrator', 'editor' ),
1009 'fields' => array( 'ID', 'user_login', 'display_name' ),
1010 ) );
1011
1012 foreach ( $users as $user ) {
1013 if ( strcasecmp( $user->display_name, $user->user_login ) === 0 ) {
1014 $exposed[] = $user->user_login;
1015 }
1016 }
1017
1018 // Cache for 12 hours
1019 set_transient( $cache_key, $exposed, 12 * HOUR_IN_SECONDS );
1020
1021 return $exposed;
1022 }
1023
1024 /**
1025 * Get accounts with insecure usernames
1026 *
1027 * Checks for common default usernames that are targeted by brute force attacks.
1028 * Detects any user regardless of role (consistent with username creation blocking).
1029 * Uses WordPress object cache via get_user_by() so no transient needed.
1030 *
1031 * @return array Array of insecure usernames found.
1032 */
1033 private function get_insecure_admin_usernames() {
1034 $priority_usernames = array( 'admin', 'administrator', 'root', 'test', 'user', 'guest', 'info', 'sysadmin', 'webmaster' );
1035 $found = array();
1036
1037 foreach ( $priority_usernames as $username ) {
1038 $user = get_user_by( 'login', $username );
1039 if ( $user ) {
1040 $found[] = $username;
1041 }
1042 }
1043
1044 return $found;
1045 }
1046
1047 /**
1048 * Get security status for menu badge
1049 *
1050 * Returns count of disabled modules and whether there are critical issues.
1051 * Critical = Firewall or Login Security disabled.
1052 *
1053 * @return array Array with 'count' and 'has_critical'.
1054 */
1055 public function get_security_status_for_badge() {
1056 $options = $this->settings->get_all_options();
1057 $modules = $options['modules'] ?? array();
1058
1059 // Count disabled modules
1060 $disabled_count = 0;
1061 $has_critical = false;
1062
1063 // Critical modules - if disabled, badge is red
1064 $critical_modules = array( 'firewall', 'login_security' );
1065
1066 foreach ( $modules as $module => $enabled ) {
1067 // Handle both boolean and string values ('1', '0', true, false)
1068 $is_enabled = filter_var( $enabled, FILTER_VALIDATE_BOOLEAN );
1069
1070 if ( ! $is_enabled ) {
1071 $disabled_count++;
1072
1073 // Check if this is a critical module
1074 if ( in_array( $module, $critical_modules, true ) ) {
1075 $has_critical = true;
1076 }
1077 }
1078 }
1079
1080 return array(
1081 'count' => $disabled_count,
1082 'has_critical' => $has_critical,
1083 );
1084 }
1085
1086 /**
1087 * Get count of security issues for menu badge (deprecated, use get_security_status_for_badge)
1088 *
1089 * @return int Count of critical/high issues.
1090 */
1091 public function get_security_issues_count() {
1092 $status = $this->get_security_status_for_badge();
1093 return $status['count'];
1094 }
1095
1096 /**
1097 * Register settings
1098 */
1099 public function register_settings() {
1100 register_setting(
1101 'vigilante_options',
1102 Vigilante_Settings::OPTION_NAME,
1103 array( $this->settings, 'validate_options' )
1104 );
1105 }
1106
1107 /**
1108 * Build the settings search index
1109 *
1110 * Flat list of searchable entries consumed by the client-side search.
1111 * Each entry contains tab + section metadata, a translated label and an
1112 * English fallback so locales with partial translation still match.
1113 *
1114 * @return array
1115 */
1116 private function get_search_index() {
1117 return array(
1118 // Firewall - Main
1119 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' ),
1120 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' ),
1121 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' ),
1122 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' ),
1123 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' ),
1124 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' ),
1125 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' ),
1126 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' ),
1127 // Firewall - Server Protection
1128 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' ),
1129 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' ),
1130 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' ),
1131 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' ),
1132 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' ),
1133 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' ),
1134 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' ),
1135 // Security Headers
1136 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' ),
1137 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' ),
1138 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' ),
1139 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' ),
1140 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' ),
1141 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' ),
1142 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' ),
1143 // Login Security
1144 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' ),
1145 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' ),
1146 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' ),
1147 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' ),
1148 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' ),
1149 // REST API
1150 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' ),
1151 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' ),
1152 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' ),
1153 // User Security
1154 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' ),
1155 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' ),
1156 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' ),
1157 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' ),
1158 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' ),
1159 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' ),
1160 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' ),
1161 // WP Hardening
1162 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' ),
1163 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' ),
1164 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' ),
1165 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' ),
1166 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' ),
1167 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' ),
1168 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' ),
1169 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' ),
1170 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' ),
1171 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' ),
1172 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' ),
1173 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' ),
1174 // File Integrity
1175 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' ),
1176 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' ),
1177 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' ),
1178 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' ),
1179 // Security Audit
1180 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' ),
1181 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' ),
1182 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' ),
1183 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' ),
1184 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' ),
1185 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' ),
1186 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' ),
1187 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' ),
1188 // Settings & Tools
1189 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' ),
1190 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' ),
1191 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' ),
1192 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' ),
1193 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' ),
1194 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' ),
1195 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' ),
1196 );
1197 }
1198
1199 /**
1200 * Enqueue admin assets
1201 *
1202 * @param string $hook Current admin page.
1203 */
1204 public function enqueue_assets( $hook ) {
1205 // toplevel_page_vigilante for top-level menu page
1206 if ( 'toplevel_page_vigilante' !== $hook ) {
1207 return;
1208 }
1209
1210 wp_enqueue_style(
1211 'vigilante-admin',
1212 VIGILANTE_ASSETS_URL . 'css/admin.css',
1213 array(),
1214 VIGILANTE_VERSION
1215 );
1216
1217 wp_enqueue_script(
1218 'vigilante-admin',
1219 VIGILANTE_ASSETS_URL . 'js/admin.js',
1220 array( 'jquery' ),
1221 VIGILANTE_VERSION,
1222 true
1223 );
1224
1225 wp_localize_script( 'vigilante-admin', 'vigilanteAdmin', array(
1226 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
1227 'nonce' => wp_create_nonce( 'vigilante_admin_nonce' ),
1228 'currentUserId' => get_current_user_id(),
1229 'logoutUrl' => wp_logout_url( wp_login_url() ),
1230 'adminUrl' => admin_url( 'admin.php?page=vigilante' ),
1231 'searchIndex' => $this->get_search_index(),
1232 'underAttack' => array(
1233 'active' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->is_active(),
1234 'remaining' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->get_remaining_time(),
1235 ),
1236 'strings' => array(
1237 'saving' => __( 'Saving...', 'vigilante' ),
1238 'sendingTest' => __( 'Sending...', 'vigilante' ),
1239 'saved' => __( 'Settings saved', 'vigilante' ),
1240 'error' => __( 'Error saving settings', 'vigilante' ),
1241 'confirm' => __( 'Are you sure?', 'vigilante' ),
1242 'scanning' => __( 'Scanning...', 'vigilante' ),
1243 'scanComplete' => __( 'Scan complete', 'vigilante' ),
1244 'loading' => __( 'Loading...', 'vigilante' ),
1245 'searching' => __( 'Searching...', 'vigilante' ),
1246 'noUsersFound' => __( 'No users found', 'vigilante' ),
1247 'searchError' => __( 'Error searching users', 'vigilante' ),
1248 'sending' => __( 'Sending...', 'vigilante' ),
1249 'sendNotification' => __( 'Send notification now', 'vigilante' ),
1250 'notificationsSent' => __( 'notifications sent', 'vigilante' ),
1251 'skipped' => __( 'skipped', 'vigilante' ),
1252 'failed' => __( 'failed', 'vigilante' ),
1253 'customConfig' => __( 'Custom Configuration', 'vigilante' ),
1254 // Header tester strings
1255 'testHeaders' => __( 'Test Headers', 'vigilante' ),
1256 'testing' => __( 'Testing...', 'vigilante' ),
1257 'score' => __( 'Score', 'vigilante' ),
1258 'enabledHeaders' => __( 'Enabled headers', 'vigilante' ),
1259 'missingHeaders' => __( 'Missing headers', 'vigilante' ),
1260 'warnings' => __( 'Warnings', 'vigilante' ),
1261 // File integrity scan results strings
1262 'scanResults' => __( 'Scan Results', 'vigilante' ),
1263 'ok' => __( 'OK', 'vigilante' ),
1264 'modified' => __( 'Modified', 'vigilante' ),
1265 'suspicious' => __( 'Suspicious', 'vigilante' ),
1266 'totalScanned' => __( 'Total Scanned', 'vigilante' ),
1267 'suspiciousFiles' => __( 'Suspicious Files', 'vigilante' ),
1268 'suspiciousWarning' => __( 'These files may contain malicious code or are in unexpected locations. Review immediately!', 'vigilante' ),
1269 'file' => __( 'File', 'vigilante' ),
1270 'reason' => __( 'Reason', 'vigilante' ),
1271 'type' => __( 'Type', 'vigilante' ),
1272 'unknown' => __( 'Unknown', 'vigilante' ),
1273 'modifiedFiles' => __( 'Modified Files', 'vigilante' ),
1274 'modifiedDescription' => __( 'These files (apparently) differ from the original WordPress or plugin versions.', 'vigilante' ),
1275 'extraFiles' => __( 'Extra Files', 'vigilante' ),
1276 'extra' => __( 'Extra', 'vigilante' ),
1277 'ignored' => __( 'Ignored', 'vigilante' ),
1278 'extraDescription' => __( 'PHP files found in plugins or themes that are not part of the original distribution from WordPress.org.', 'vigilante' ),
1279 'actions' => __( 'Actions', 'vigilante' ),
1280 'ignore' => __( 'Ignore', 'vigilante' ),
1281 'ignoring' => __( 'Ignoring...', 'vigilante' ),
1282 'fileIgnored' => __( 'File added to ignored list.', 'vigilante' ),
1283 'fileUnignored' => __( 'File removed from ignored list.', 'vigilante' ),
1284 'confirmClearIgnored' => __( 'Remove all files from the ignored list? They will appear in scan results again.', 'vigilante' ),
1285 'ignoredCleared' => __( 'Ignored files list cleared. Page will reload...', 'vigilante' ),
1286 'selectAll' => __( 'Select all', 'vigilante' ),
1287 'bulkIgnoreSelected' => __( 'Ignore selected', 'vigilante' ),
1288 'bulkUnignoreSelected'=> __( 'Stop ignoring selected', 'vigilante' ),
1289 'bulkNoSelection' => __( 'Select at least one file first.', 'vigilante' ),
1290 'bulkConfirmIgnore' => __( 'Ignore the selected files? They will be hidden from future scan results until you remove them from the ignored list.', 'vigilante' ),
1291 'bulkConfirmUnignore' => __( 'Remove the selected files from the ignored list? They will appear in scan results again.', 'vigilante' ),
1292 'bulkProcessing' => __( 'Processing...', 'vigilante' ),
1293 /* translators: %d: number of files selected for bulk action. */
1294 'bulkSelectedCount' => __( '%d selected', 'vigilante' ),
1295 'allClear' => __( 'All files verified - no issues found!', 'vigilante' ),
1296 'criticalConfigTitle' => __( 'Critical config files modified', 'vigilante' ),
1297 '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' ),
1298 'approve' => __( 'Approve', 'vigilante' ),
1299 'approving' => __( 'Approving...', 'vigilante' ),
1300 'criticalApproved' => __( 'Change approved. Next scan will use the current state as baseline.', 'vigilante' ),
1301 'reviewChanges' => __( 'Review changes', 'vigilante' ),
1302 'hideChanges' => __( 'Hide changes', 'vigilante' ),
1303 'changes' => __( 'Changes', 'vigilante' ),
1304 'diffUnavailable' => __( 'Diff not available for this file (baseline was created before diff tracking was added). Approve to enable diff on future changes.', 'vigilante' ),
1305 'diffEmpty' => __( 'No line-level changes detected (may be whitespace or reordering).', 'vigilante' ),
1306 'diffLines' => __( 'lines', 'vigilante' ),
1307 // Under Attack mode strings
1308 'underAttackConfirmActivate' => __( 'Activate Under Attack mode? All visitors will see a verification page for the next 4 hours.', 'vigilante' ),
1309 'underAttackConfirmDeactivate' => __( 'Deactivate Under Attack mode?', 'vigilante' ),
1310 'underAttackActivating' => __( 'Activating...', 'vigilante' ),
1311 'underAttackDeactivating' => __( 'Deactivating...', 'vigilante' ),
1312 // Database backup strings
1313 'dbBackupDownloading' => __( 'Generating backup...', 'vigilante' ),
1314 'dbBackupNoTables' => __( 'Please select at least one table.', 'vigilante' ),
1315 'dbBackupSuccess' => __( 'Database backup downloaded successfully.', 'vigilante' ),
1316 // Firewall unblock
1317 'confirmUnblockIp' => __( 'Unblock this IP from firewall rate limiting?', 'vigilante' ),
1318 // Database prefix strings
1319 'dbPrefixConfirm' => __( 'This operation will change your database prefix. It is irreversible. Make sure you have a current database backup before proceeding.', 'vigilante' ),
1320 'dbPrefixChanging' => __( 'Changing prefix...', 'vigilante' ),
1321 'dbPrefixSuccess' => __( 'Database prefix changed successfully. The page will reload now.', 'vigilante' ),
1322 'dbPrefixCheckbox' => __( 'You must confirm that you have a database backup.', 'vigilante' ),
1323 /* translators: 1: Hours, 2: Minutes */
1324 'underAttackRemaining' => __( '%1$dh %2$dm remaining', 'vigilante' ),
1325 'underAttackLabel' => __( 'Under Attack', 'vigilante' ),
1326 'standardLabel' => __( 'Standard', 'vigilante' ),
1327 'maximumLabel' => __( 'Maximum Security', 'vigilante' ),
1328 'deactivate' => __( 'Deactivate', 'vigilante' ),
1329 'underAttackActivate' => __( 'Activate for 4 hours', 'vigilante' ),
1330 // Settings strings
1331 'saveSettings' => __( 'Save Settings', 'vigilante' ),
1332 'settingsResetDefaults' => __( 'Settings reset to defaults.', 'vigilante' ),
1333 'confirmOverwrite' => __( 'This will overwrite your current settings.', 'vigilante' ),
1334 'importFailed' => __( 'Could not import settings. Check the file and try again.', 'vigilante' ),
1335 /* translators: 1: tests passed, 2: total tests in this category */
1336 'testsCounter' => __( '%1$d/%2$d tests', 'vigilante' ),
1337 'confirmResetAll' => __( 'This will reset ALL settings to defaults.', 'vigilante' ),
1338 'couldNotDetermineSection' => __( 'Could not determine section.', 'vigilante' ),
1339 'confirmResetSection' => __( 'Reset this section to default values? This cannot be undone.', 'vigilante' ),
1340 'sectionResetDefaults' => __( 'Section reset to defaults.', 'vigilante' ),
1341 /* translators: %s: preset name */
1342 'confirmApplyPreset' => __( 'Apply the "%s" preset?', 'vigilante' ),
1343 // Scan strings
1344 'scanFailed' => __( 'Scan failed', 'vigilante' ),
1345 /* translators: %s: error message */
1346 'scanError' => __( 'Scan error: %s', 'vigilante' ),
1347 'runScanNow' => __( 'Run Scan Now', 'vigilante' ),
1348 'confirmClearScan' => __( 'Are you sure you want to clear all scan results?', 'vigilante' ),
1349 'clearing' => __( 'Clearing...', 'vigilante' ),
1350 'scanResultsCleared' => __( 'Scan results cleared. Page will reload...', 'vigilante' ),
1351 'failedClearResults' => __( 'Failed to clear results', 'vigilante' ),
1352 /* translators: %s: error message */
1353 'ajaxError' => __( 'AJAX Error: %s', 'vigilante' ),
1354 // Activity log popup strings
1355 'logRequest' => __( 'Request', 'vigilante' ),
1356 'logDate' => __( 'Date', 'vigilante' ),
1357 'logMethod' => __( 'Method', 'vigilante' ),
1358 'logType' => __( 'Type', 'vigilante' ),
1359 'logAction' => __( 'Action', 'vigilante' ),
1360 'logSeverity' => __( 'Severity', 'vigilante' ),
1361 'logMessage' => __( 'Message', 'vigilante' ),
1362 'logClient' => __( 'Client', 'vigilante' ),
1363 'logUser' => __( 'User', 'vigilante' ),
1364 'logIpAddress' => __( 'IP Address', 'vigilante' ),
1365 'logUserAgent' => __( 'User Agent', 'vigilante' ),
1366 'logIpLabel' => __( 'IP:', 'vigilante' ),
1367 'logUaLabel' => __( 'UA:', 'vigilante' ),
1368 'logWhitelist' => __( 'Whitelist', 'vigilante' ),
1369 'logBlacklist' => __( 'Blacklist', 'vigilante' ),
1370 'logInWhitelist' => __( 'In whitelist', 'vigilante' ),
1371 'logInBlacklist' => __( 'In blacklist', 'vigilante' ),
1372 'logAdded' => __( 'Added!', 'vigilante' ),
1373 'logErrorAddingToList' => __( 'Error adding to list', 'vigilante' ),
1374 'logRequestFailed' => __( 'Request failed', 'vigilante' ),
1375 // Activity log table strings
1376 'noLogEntries' => __( 'No log entries found.', 'vigilante' ),
1377 'view' => __( 'View', 'vigilante' ),
1378 'confirmClearLogs' => __( 'This will delete all audit logs.', 'vigilante' ),
1379 // Export logs strings
1380 'exporting' => __( 'Exporting...', 'vigilante' ),
1381 /* translators: %d: number of entries */
1382 'logsExported' => __( 'Logs exported (%d entries)', 'vigilante' ),
1383 'noLogsToExport' => __( 'No logs to export', 'vigilante' ),
1384 'exportFailed' => __( 'Export failed', 'vigilante' ),
1385 'exportLogs' => __( 'Export Logs', 'vigilante' ),
1386 // Backup strings
1387 'backupCreated' => __( 'Backup created successfully.', 'vigilante' ),
1388 'createBackupNow' => __( 'Download Backup', 'vigilante' ),
1389 'downloadBackup' => __( 'Download Backup (.zip)', 'vigilante' ),
1390 /* translators: %d: number of tables */
1391 'tablesCount' => __( '%d tables', 'vigilante' ),
1392 /* translators: 1: table count, 2: human-readable size */
1393 'dbTablesTotal' => __( '%1$d tables total (%2$s)', 'vigilante' ),
1394 /* translators: 1: selected count, 2: human-readable size */
1395 'dbTablesSelected' => __( '%1$d tables selected (%2$s)', 'vigilante' ),
1396 // Settings search strings
1397 'searchNoResults' => __( 'No matching settings found.', 'vigilante' ),
1398 'searchInTab' => __( 'in', 'vigilante' ),
1399 // Modules string
1400 /* translators: 1: enabled count, 2: total count */
1401 'modulesEnabled' => __( '%1$d / %2$d modules enabled', 'vigilante' ),
1402 // Activity log label maps for JS rendering
1403 'eventTypeLabels' => array(
1404 'login' => __( 'Login', 'vigilante' ),
1405 'user' => __( 'User', 'vigilante' ),
1406 'content' => __( 'Content', 'vigilante' ),
1407 'plugin' => __( 'Plugin', 'vigilante' ),
1408 'theme' => __( 'Theme', 'vigilante' ),
1409 'settings' => __( 'Settings', 'vigilante' ),
1410 'comment' => __( 'Comment', 'vigilante' ),
1411 'media' => __( 'Media', 'vigilante' ),
1412 'firewall' => __( 'Firewall', 'vigilante' ),
1413 'file' => __( 'File', 'vigilante' ),
1414 'security' => __( 'Security', 'vigilante' ),
1415 'system' => __( 'System', 'vigilante' ),
1416 ),
1417 'severityLabels' => array(
1418 'info' => __( 'Info', 'vigilante' ),
1419 'warning' => __( 'Warning', 'vigilante' ),
1420 'critical' => __( 'Critical', 'vigilante' ),
1421 ),
1422 // Password reset strings
1423 'noUsersFoundSearch' => __( 'No users found', 'vigilante' ),
1424 /* translators: %d: number of users */
1425 'confirmForceReset' => __( 'Force password reset for %d user(s)? A password reset email will be sent to each user.', 'vigilante' ),
1426 'warningResettingSelf' => __( 'WARNING: You are including yourself. Your session will end and you will need to set a new password.', 'vigilante' ),
1427 'processing' => __( 'Processing...', 'vigilante' ),
1428 'anErrorOccurred' => __( 'An error occurred', 'vigilante' ),
1429 'forceResetSelected' => __( 'Force Reset for Selected Users', 'vigilante' ),
1430 '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' ),
1431 'warningResettingSelfAll' => __( 'WARNING: You are including yourself. Your session will end immediately.', 'vigilante' ),
1432 'forceResetAll' => __( 'Force Reset for ALL Users', 'vigilante' ),
1433 // Role-based password reset strings
1434 /* translators: %d: number of users */
1435 'confirmForceResetByRole' => __( 'Force password reset for %d user(s) with the selected roles? A password reset email will be sent to each user.', 'vigilante' ),
1436 'noRolesSelected' => __( 'Please select at least one role.', 'vigilante' ),
1437 'forceResetByRole' => __( 'Force Reset for Selected Roles', 'vigilante' ),
1438 // User approval strings
1439 'confirmApprove' => __( 'Approve this user?', 'vigilante' ),
1440 'approve' => __( 'Approve', 'vigilante' ),
1441 'rejectReason' => __( 'Enter rejection reason (optional):', 'vigilante' ),
1442 'reject' => __( 'Reject', 'vigilante' ),
1443 'noPending' => __( 'No pending registrations.', 'vigilante' ),
1444 // Session management strings
1445 'confirmRevoke' => __( 'Revoke this session?', 'vigilante' ),
1446 'confirmRevokeAll' => __( 'Revoke all other sessions?', 'vigilante' ),
1447 'revokeOthers' => __( 'Revoke All Other Sessions', 'vigilante' ),
1448 'confirmRevokeAllUser' => __( 'Revoke ALL sessions for this user? They will be logged out everywhere.', 'vigilante' ),
1449 'sessionsFor' => __( 'Sessions for:', 'vigilante' ),
1450 'noSessions' => __( 'No active sessions', 'vigilante' ),
1451 'revoke' => __( 'Revoke', 'vigilante' ),
1452 'noUsers' => __( 'No users found', 'vigilante' ),
1453 // Time ago strings
1454 'timeYear' => __( 'year', 'vigilante' ),
1455 'timeYears' => __( 'years', 'vigilante' ),
1456 'timeMonth' => __( 'month', 'vigilante' ),
1457 'timeMonths' => __( 'months', 'vigilante' ),
1458 'timeDay' => __( 'day', 'vigilante' ),
1459 'timeDays' => __( 'days', 'vigilante' ),
1460 'timeHour' => __( 'hour', 'vigilante' ),
1461 'timeHours' => __( 'hours', 'vigilante' ),
1462 'timeMinute' => __( 'minute', 'vigilante' ),
1463 'timeMinutes' => __( 'minutes', 'vigilante' ),
1464 /* translators: %1$d: count, %2$s: time unit */
1465 'timeAgo' => __( '%1$d %2$s ago', 'vigilante' ),
1466 'justNow' => __( 'Just now', 'vigilante' ),
1467 // Pagination strings
1468 /* translators: 1: first item number, 2: last item number, 3: total items */
1469 'paginationOf' => __( '%1$d–%2$d of %3$d', 'vigilante' ),
1470 'paginationEmpty' => __( '0 items', 'vigilante' ),
1471 // Security Analyzer strings
1472 'analyzerScanNow' => __( 'Scan now', 'vigilante' ),
1473 'analyzerScanning' => __( 'Scanning…', 'vigilante' ),
1474 'analyzerFastPhase' => __( 'Running fast checks…', 'vigilante' ),
1475 'analyzerSlowPhase' => __( 'Running remote checks…', 'vigilante' ),
1476 'analyzerScanComplete' => __( 'Security scan complete.', 'vigilante' ),
1477 'analyzerScanFailed' => __( 'Security scan failed.', 'vigilante' ),
1478 'analyzerShowDetails' => __( 'Show detailed breakdown', 'vigilante' ),
1479 'analyzerHideDetails' => __( 'Hide detailed breakdown', 'vigilante' ),
1480 'analyzerGoToSetting' => __( 'Go to setting', 'vigilante' ),
1481 'analyzerNoData' => __( 'No data yet — run a scan to populate this category.', 'vigilante' ),
1482 'analyzerJustNow' => __( 'just now', 'vigilante' ),
1483 'analyzerAgo' => __( 'ago', 'vigilante' ),
1484 'analyzerSettingsSaved' => __( 'Analyzer settings saved.', 'vigilante' ),
1485 'analyzerLastScanJustNow' => __( 'Last scan just now', 'vigilante' ),
1486 'analyzerQualityExcellent' => __( 'Excellent', 'vigilante' ),
1487 'analyzerQualityGood' => __( 'Good', 'vigilante' ),
1488 'analyzerQualityFair' => __( 'Fair', 'vigilante' ),
1489 'analyzerQualityPoor' => __( 'Poor', 'vigilante' ),
1490 'analyzerQualityCritical' => __( 'Critical', 'vigilante' ),
1491 'analyzerPts' => __( 'pts', 'vigilante' ),
1492 'analyzerLearnMore' => __( 'Learn more', 'vigilante' ),
1493 'analyzerInfoAllClear' => __( 'All clear', 'vigilante' ),
1494 /* translators: %d: number of findings in an info-only category */
1495 'analyzerInfoFindings' => __( '%d findings', 'vigilante' ),
1496 ),
1497 ) );
1498
1499 // 2FA Admin assets
1500 wp_enqueue_style(
1501 'vigilante-2fa-admin',
1502 VIGILANTE_ASSETS_URL . 'css/two-factor-admin.css',
1503 array( 'vigilante-admin' ),
1504 VIGILANTE_VERSION
1505 );
1506
1507 wp_enqueue_script(
1508 'vigilante-2fa-admin',
1509 VIGILANTE_ASSETS_URL . 'js/two-factor-admin.js',
1510 array( 'jquery', 'vigilante-admin' ),
1511 VIGILANTE_VERSION,
1512 true
1513 );
1514 }
1515
1516 /**
1517 * Show admin notices
1518 */
1519 public function show_admin_notices() {
1520 // Activation notice
1521 if ( get_transient( 'vigilante_activated' ) ) {
1522 ?>
1523 <div class="notice notice-success is-dismissible vigilante-activation-notice">
1524 <p>
1525 <span class="dashicons dashicons-shield vigilante-notice-icon"></span>
1526 <strong><?php esc_html_e( 'Vigilant activated successfully!', 'vigilante' ); ?></strong>
1527 <?php esc_html_e( 'Security protection is now active.', 'vigilante' ); ?>
1528 <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante' ) ); ?>">
1529 <?php esc_html_e( 'Configure settings', 'vigilante' ); ?>
1530 </a>
1531 </p>
1532 </div>
1533 <?php
1534 delete_transient( 'vigilante_activated' );
1535 }
1536
1537 // Under Attack mode notice (non-dismissible, shown on all admin pages)
1538 $ua_status = get_option( Vigilante_Under_Attack::OPTION_NAME, array() );
1539 if ( ! empty( $ua_status['active'] ) ) {
1540 $ua_remaining = ( $ua_status['activated_at'] + $ua_status['duration'] ) - time();
1541 if ( $ua_remaining > 0 ) {
1542 $ua_hours = floor( $ua_remaining / 3600 );
1543 $ua_mins = floor( ( $ua_remaining % 3600 ) / 60 );
1544 $dashboard_url = admin_url( 'admin.php?page=vigilante' );
1545 ?>
1546 <div class="notice notice-warning vigilante-ua-notice">
1547 <p>
1548 <span class="dashicons dashicons-shield"></span>
1549 <strong><?php esc_html_e( 'Under Attack mode is active', 'vigilante' ); ?></strong>
1550 &mdash;
1551 <?php
1552 printf(
1553 /* translators: 1: Hours, 2: Minutes */
1554 esc_html__( '%1$dh %2$dm remaining.', 'vigilante' ),
1555 absint( $ua_hours ),
1556 absint( $ua_mins )
1557 );
1558 ?>
1559 <a href="<?php echo esc_url( $dashboard_url ); ?>">
1560 <?php esc_html_e( 'Go to Vigilant dashboard', 'vigilante' ); ?>
1561 </a>
1562 </p>
1563 <p>
1564 <em><?php esc_html_e( 'Vigilant has applied the Maximum preset plus extra hardening on top of your previous configuration. Any changes you make to Vigilant settings while this mode is active will be reverted when it ends.', 'vigilante' ); ?></em>
1565 </p>
1566 </div>
1567 <?php
1568 }
1569 }
1570
1571 // Proxy/CDN detection: if IP detection is set to "direct" but requests
1572 // arrive with a forwarded-for header carrying a different valid IP, the
1573 // site is very likely behind a proxy/CDN that has not been declared, so
1574 // the firewall is seeing the proxy IP for every visitor. Guide the admin.
1575 //
1576 // Two deliberate silencers (2.9.2):
1577 // - A loopback forwarded IP (::1 / 127.x) means the person browsing IS
1578 // the machine itself: that only happens in local development stacks
1579 // (Local's nginx router, Docker...), never behind a production
1580 // proxy/CDN, so the notice would be pure noise there.
1581 // - The notice is dismissible and the dismissal persists site-wide via
1582 // the vigilante_dismissed_notices option (the admin evaluated it and
1583 // decided; nagging forever helps nobody).
1584 $dismissed_notices = get_option( 'vigilante_dismissed_notices', array() );
1585 if (
1586 current_user_can( 'manage_options' )
1587 && '' === Vigilante_IP_Utils::trusted_proxy_header()
1588 && ! isset( $dismissed_notices['proxy_detection'] )
1589 ) {
1590 $remote = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
1591 $detected = '';
1592 foreach ( Vigilante_IP_Utils::trusted_header_map() as $proxy_label => $server_key ) {
1593 if ( empty( $_SERVER[ $server_key ] ) ) {
1594 continue;
1595 }
1596 $candidate = sanitize_text_field( wp_unslash( $_SERVER[ $server_key ] ) );
1597 if ( false !== strpos( $candidate, ',' ) ) {
1598 $parts = explode( ',', $candidate );
1599 $candidate = trim( $parts[0] );
1600 }
1601 if ( ! filter_var( $candidate, FILTER_VALIDATE_IP ) || $candidate === $remote ) {
1602 continue;
1603 }
1604 // Loopback forwarded IP = local development, not a real proxy.
1605 if ( '::1' === $candidate || 0 === strpos( $candidate, '127.' ) ) {
1606 continue;
1607 }
1608 $detected = $proxy_label;
1609 break;
1610 }
1611
1612 if ( '' !== $detected ) {
1613 $firewall_url = admin_url( 'admin.php?page=vigilante&tab=firewall' );
1614 $detail_message = sprintf(
1615 /* translators: %s: detected forwarded header name wrapped in a code tag. */
1616 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' ),
1617 '<code>' . esc_html( strtoupper( $detected ) ) . '</code>'
1618 );
1619 ?>
1620 <div class="notice notice-warning is-dismissible vigilante-proxy-notice">
1621 <p>
1622 <span class="dashicons dashicons-shield"></span>
1623 <strong><?php esc_html_e( 'Vigilant: this site looks like it is behind a proxy or CDN', 'vigilante' ); ?></strong>
1624 </p>
1625 <p><?php echo wp_kses_post( $detail_message ); ?></p>
1626 <p>
1627 <a href="<?php echo esc_url( $firewall_url ); ?>" class="button button-secondary">
1628 <?php esc_html_e( 'Set your proxy in Firewall settings', 'vigilante' ); ?>
1629 </a>
1630 </p>
1631 </div>
1632 <script>
1633 jQuery( function ( $ ) {
1634 $( document ).on( 'click', '.vigilante-proxy-notice .notice-dismiss', function () {
1635 $.post( ajaxurl, {
1636 action: 'vigilante_dismiss_notice',
1637 notice_id: 'proxy_detection',
1638 nonce: <?php echo wp_json_encode( wp_create_nonce( 'vigilante_dismiss_notice' ) ); ?>
1639 } );
1640 } );
1641 } );
1642 </script>
1643 <?php
1644 }
1645 }
1646 }
1647
1648 /**
1649 * Render settings page
1650 */
1651 public function render_settings_page() {
1652 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1653 $this->current_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'dashboard';
1654
1655 if ( ! array_key_exists( $this->current_tab, $this->tabs ) ) {
1656 $this->current_tab = 'dashboard';
1657 }
1658 ?>
1659 <div class="wrap vigilante-admin-wrap">
1660 <div class="vigilante-header-row">
1661 <h1 class="vigilante-page-title">
1662 <img src="<?php echo esc_url( VIGILANTE_ASSETS_URL . 'images/icon.png' ); ?>" alt="Vigilante" class="vigilante-title-icon">
1663 <?php esc_html_e( 'Vigilant', 'vigilante' ); ?>
1664 <span class="vigilante-version">v<?php echo esc_html( VIGILANTE_VERSION ); ?></span>
1665 </h1>
1666 <div class="vigilante-search-wrapper">
1667 <div class="vigilante-search-input-wrap">
1668 <span class="vigilante-search-icon dashicons dashicons-search" aria-hidden="true"></span>
1669 <input type="search" id="vigilante-settings-search" class="vigilante-settings-search" placeholder="<?php esc_attr_e( 'Search settings…', 'vigilante' ); ?>" autocomplete="off">
1670 <span class="vigilante-search-shortcut" aria-hidden="true">/</span>
1671 </div>
1672 <div id="vigilante-settings-search-results" class="vigilante-search-results" hidden role="listbox"></div>
1673 </div>
1674 </div>
1675
1676 <?php $this->render_tabs(); ?>
1677
1678 <div class="vigilante-content">
1679 <div class="vigilante-main">
1680 <?php $this->render_tab_content(); ?>
1681 </div>
1682 <div class="vigilante-sidebar">
1683 <?php $this->render_sidebar(); ?>
1684 </div>
1685 </div>
1686 </div>
1687 <?php
1688 }
1689
1690 /**
1691 * Render tabs navigation
1692 */
1693 private function render_tabs() {
1694 $options = $this->settings->get_all_options();
1695
1696 // Map tabs to modules
1697 $tab_to_module = array(
1698 'firewall' => 'firewall',
1699 'headers' => 'security_headers',
1700 'login' => 'login_security',
1701 'rest-api' => 'rest_api_security',
1702 'users' => 'user_security',
1703 'wp-hardening' => 'wp_hardening',
1704 'file-integrity' => 'file_integrity',
1705 'activity-log' => 'activity_log',
1706 );
1707 ?>
1708 <nav class="nav-tab-wrapper vigilante-nav-tabs">
1709 <?php foreach ( $this->tabs as $tab_id => $tab_name ) :
1710 $is_disabled = false;
1711 $module = isset( $tab_to_module[ $tab_id ] ) ? $tab_to_module[ $tab_id ] : null;
1712 if ( $module && empty( $options['modules'][ $module ] ) ) {
1713 $is_disabled = true;
1714 }
1715 ?>
1716 <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=' . $tab_id ) ); ?>"
1717 class="nav-tab <?php echo $this->current_tab === $tab_id ? 'nav-tab-active' : ''; ?> <?php echo $is_disabled ? 'vigilante-tab-disabled' : ''; ?>"
1718 <?php if ( $is_disabled ) : ?>title="<?php esc_attr_e( 'Module Disabled', 'vigilante' ); ?>"<?php endif; ?>>
1719 <?php echo esc_html( $tab_name ); ?>
1720 <?php if ( $is_disabled ) : ?><span class="vigilante-tab-off">OFF</span><?php endif; ?>
1721 </a>
1722 <?php endforeach; ?>
1723 </nav>
1724 <?php
1725 }
1726
1727 /**
1728 * Render current tab content
1729 */
1730 private function render_tab_content() {
1731 $method = 'render_tab_' . str_replace( '-', '_', $this->current_tab );
1732
1733 if ( method_exists( $this, $method ) ) {
1734 $this->$method();
1735 } else {
1736 $this->render_tab_coming_soon();
1737 }
1738 }
1739
1740 /**
1741 * Render coming soon placeholder
1742 */
1743 private function render_tab_coming_soon() {
1744 ?>
1745 <div class="vigilante-settings-section">
1746 <h2><?php esc_html_e( 'Coming Soon', 'vigilante' ); ?></h2>
1747 <p><?php esc_html_e( 'This section is under development.', 'vigilante' ); ?></p>
1748 </div>
1749 <?php
1750 }
1751
1752 /**
1753 * Check if module is disabled and render warning
1754 *
1755 * @param string $module_key Module key.
1756 * @return bool True if disabled.
1757 */
1758 private function render_module_disabled_notice( $module_key ) {
1759 if ( $this->settings->is_module_enabled( $module_key ) ) {
1760 return false;
1761 }
1762
1763 $module_labels = $this->settings->get_module_labels();
1764 $module_name = isset( $module_labels[ $module_key ] ) ? $module_labels[ $module_key ] : $module_key;
1765 ?>
1766 <div class="notice notice-warning vigilante-module-disabled-notice">
1767 <p>
1768 <strong><?php esc_html_e( 'Module Disabled', 'vigilante' ); ?></strong> -
1769 <?php
1770 printf(
1771 /* translators: %s: Module name */
1772 esc_html__( 'The %s module is currently disabled. Enable it from the Dashboard to use these settings.', 'vigilante' ),
1773 esc_html( $module_name )
1774 );
1775 ?>
1776 </p>
1777 <p>
1778 <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=dashboard' ) ); ?>" class="button">
1779 <?php esc_html_e( 'Go to Dashboard', 'vigilante' ); ?>
1780 </a>
1781 </p>
1782 </div>
1783 <?php
1784 return true;
1785 }
1786
1787 /**
1788 * Render the Security Analyzer (Security Check) widget + expandable full report.
1789 *
1790 * Lives in the Dashboard tab between the Configuration Score status card and
1791 * the modules grid. Uses the last persisted scan to hydrate server-side so the
1792 * first paint shows real data; "Scan now" runs the 2-phase AJAX to refresh.
1793 *
1794 * @param array $last_scan Result of Vigilante_Security_Analyzer::get_last_scan().
1795 * @param array $history Score history (oldest first).
1796 * @param array $categories_def Category metadata (slug => label/max).
1797 * @param array $analyzer_settings Settings subsection for weekly cron + email.
1798 */
1799 private function render_analyzer_widget( $last_scan, $history, $categories_def, $analyzer_settings ) {
1800 $has_data = ! empty( $last_scan['ran_at'] );
1801 $score = isset( $last_scan['score'] ) ? (int) $last_scan['score'] : 0;
1802 $grade = isset( $last_scan['grade'] ) ? (string) $last_scan['grade'] : '';
1803 $counts = isset( $last_scan['counts'] ) && is_array( $last_scan['counts'] ) ? $last_scan['counts'] : array();
1804 $ran_at_human = $has_data
1805 ? sprintf(
1806 /* translators: %s: relative time like "2 hours" */
1807 __( 'Last scan %s ago', 'vigilante' ),
1808 human_time_diff( (int) $last_scan['ran_at'], time() )
1809 )
1810 : __( 'Never scanned', 'vigilante' );
1811 $categories = isset( $last_scan['categories'] ) && is_array( $last_scan['categories'] ) ? $last_scan['categories'] : array();
1812 $weekly_enabled = ! isset( $analyzer_settings['weekly_scan_enabled'] ) || ! empty( $analyzer_settings['weekly_scan_enabled'] );
1813 $email_enabled = ! empty( $analyzer_settings['email_on_regression'] );
1814
1815 $quality = self::analyzer_quality_tag( $score );
1816 ?>
1817 <div class="vigilante-analyzer" id="vigilante-analyzer"
1818 data-has-data="<?php echo $has_data ? '1' : '0'; ?>">
1819 <div class="vigilante-analyzer-header">
1820 <div class="vigilante-analyzer-title">
1821 <h2>
1822 <span class="dashicons dashicons-shield-alt" aria-hidden="true"></span>
1823 <?php esc_html_e( 'Security Check', 'vigilante' ); ?>
1824 </h2>
1825 <p class="description">
1826 <?php esc_html_e( 'On-demand audit of what an attacker would see right now, plus 13 internal checks impossible from the outside.', 'vigilante' ); ?>
1827 </p>
1828 <p class="vigilante-analyzer-last-scan" data-role="ran-at">
1829 <span class="dashicons dashicons-clock" aria-hidden="true"></span>
1830 <?php echo esc_html( $ran_at_human ); ?>
1831 </p>
1832 </div>
1833 <div class="vigilante-analyzer-actions">
1834 <button type="button" class="button button-primary" id="vigilante-analyzer-scan">
1835 <span class="dashicons dashicons-update" aria-hidden="true"></span>
1836 <?php esc_html_e( 'Scan now', 'vigilante' ); ?>
1837 </button>
1838 </div>
1839 </div>
1840
1841 <div class="vigilante-analyzer-summary">
1842 <div class="vigilante-analyzer-score-card">
1843 <?php if ( $has_data && $grade ) : ?>
1844 <div class="vigilante-score-circle vigilante-grade-<?php echo esc_attr( strtolower( $grade ) ); ?>">
1845 <span class="vigilante-grade"><?php echo esc_html( $grade ); ?></span>
1846 <span class="vigilante-score-text"><?php echo esc_html( $score ); ?>%</span>
1847 </div>
1848 <?php else : ?>
1849 <div class="vigilante-score-circle vigilante-grade-empty">
1850 <span class="vigilante-grade">—</span>
1851 <span class="vigilante-score-text"><?php esc_html_e( 'N/A', 'vigilante' ); ?></span>
1852 </div>
1853 <?php endif; ?>
1854 <div class="vigilante-analyzer-score-meta">
1855 <p class="vigilante-analyzer-score-label">
1856 <?php esc_html_e( 'Security Score', 'vigilante' ); ?>
1857 </p>
1858 <span class="vigilante-analyzer-quality-tag vigilante-analyzer-quality-<?php echo esc_attr( $quality['slug'] ); ?>"
1859 data-role="quality-tag">
1860 <?php echo esc_html( $quality['label'] ); ?>
1861 </span>
1862 </div>
1863 </div>
1864
1865 <div class="vigilante-analyzer-counts">
1866 <span class="vigilante-analyzer-count vigilante-analyzer-count--pass">
1867 <span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>
1868 <strong data-role="pass"><?php echo esc_html( isset( $counts['pass'] ) ? $counts['pass'] : 0 ); ?></strong>
1869 <span class="vigilante-analyzer-count-label"><?php esc_html_e( 'Passed', 'vigilante' ); ?></span>
1870 </span>
1871 <span class="vigilante-analyzer-count vigilante-analyzer-count--warn">
1872 <span class="dashicons dashicons-warning" aria-hidden="true"></span>
1873 <strong data-role="warn"><?php echo esc_html( isset( $counts['warn'] ) ? $counts['warn'] : 0 ); ?></strong>
1874 <span class="vigilante-analyzer-count-label"><?php esc_html_e( 'Warnings', 'vigilante' ); ?></span>
1875 </span>
1876 <span class="vigilante-analyzer-count vigilante-analyzer-count--fail">
1877 <span class="dashicons dashicons-dismiss" aria-hidden="true"></span>
1878 <strong data-role="fail"><?php echo esc_html( isset( $counts['fail'] ) ? $counts['fail'] : 0 ); ?></strong>
1879 <span class="vigilante-analyzer-count-label"><?php esc_html_e( 'Failing', 'vigilante' ); ?></span>
1880 </span>
1881 </div>
1882
1883 <?php
1884 // Sparkline of recent scores. Require at least 3 data points so the trend
1885 // is meaningful (2 points is just a line between dots, no real trend).
1886 $hist_points = array();
1887 foreach ( $history as $h ) {
1888 $hist_points[] = (int) $h['score'];
1889 }
1890 $hist_count = count( $hist_points );
1891 if ( $hist_count >= 3 ) :
1892 $current_score = (int) end( $hist_points );
1893 $previous_score = (int) $hist_points[ $hist_count - 2 ];
1894 $delta = $current_score - $previous_score;
1895 $delta_class = $delta > 0 ? 'vigilante-analyzer-delta--up' : ( $delta < 0 ? 'vigilante-analyzer-delta--down' : 'vigilante-analyzer-delta--flat' );
1896 $delta_icon = $delta > 0 ? 'arrow-up-alt' : ( $delta < 0 ? 'arrow-down-alt' : 'minus' );
1897 if ( 0 === $delta ) {
1898 $delta_text = __( 'No change', 'vigilante' );
1899 } else {
1900 $delta_text = sprintf(
1901 /* translators: %s: signed delta, e.g. "+3" or "-5" */
1902 _n( '%s pt vs. previous scan', '%s pts vs. previous scan', abs( $delta ), 'vigilante' ),
1903 ( $delta > 0 ? '+' : '' ) . (int) $delta
1904 );
1905 }
1906 ?>
1907 <div class="vigilante-analyzer-sparkline-wrap">
1908 <div class="vigilante-analyzer-sparkline-head">
1909 <span class="vigilante-analyzer-sparkline-label">
1910 <?php
1911 echo esc_html( sprintf(
1912 /* translators: %d: number of scans */
1913 _n( 'Score trend (last %d scan)', 'Score trend (last %d scans)', $hist_count, 'vigilante' ),
1914 $hist_count
1915 ) );
1916 ?>
1917 </span>
1918 <span class="vigilante-analyzer-delta <?php echo esc_attr( $delta_class ); ?>">
1919 <span class="dashicons dashicons-<?php echo esc_attr( $delta_icon ); ?>" aria-hidden="true"></span>
1920 <?php echo esc_html( $delta_text ); ?>
1921 </span>
1922 </div>
1923 <div class="vigilante-analyzer-sparkline" data-role="sparkline"
1924 data-points="<?php echo esc_attr( wp_json_encode( $hist_points ) ); ?>">
1925 <?php echo self::sparkline_svg( $hist_points ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Safe SVG from helper ?>
1926 </div>
1927 </div>
1928 <?php elseif ( $has_data ) : ?>
1929 <div class="vigilante-analyzer-sparkline-wrap vigilante-analyzer-sparkline-wrap--placeholder">
1930 <span class="vigilante-analyzer-sparkline-label">
1931 <?php esc_html_e( 'Score trend', 'vigilante' ); ?>
1932 </span>
1933 <p class="vigilante-analyzer-sparkline-hint">
1934 <span class="dashicons dashicons-chart-line" aria-hidden="true"></span>
1935 <?php
1936 $needed = 3 - $hist_count;
1937 echo esc_html( sprintf(
1938 /* translators: %d: number of additional scans needed */
1939 _n( '%d more scan needed to show a trend.', '%d more scans needed to show a trend.', $needed, 'vigilante' ),
1940 $needed
1941 ) );
1942 ?>
1943 </p>
1944 </div>
1945 <?php endif; ?>
1946 </div>
1947
1948 <div class="vigilante-analyzer-toggle-row">
1949 <button type="button" class="button-link vigilante-analyzer-toggle" aria-expanded="false">
1950 <?php esc_html_e( 'Show detailed breakdown', 'vigilante' ); ?>
1951 <span class="vigilante-analyzer-toggle-chevron" aria-hidden="true"></span>
1952 </button>
1953 </div>
1954
1955 <div class="vigilante-analyzer-details" hidden>
1956 <div class="vigilante-analyzer-categories" data-role="categories">
1957 <?php foreach ( $categories_def as $slug => $meta ) :
1958 $cat = isset( $categories[ $slug ] ) ? $categories[ $slug ] : array();
1959 $earned = isset( $cat['earned'] ) ? (int) $cat['earned'] : 0;
1960 // Always use the declared meta as the source of truth for the maximum.
1961 // The cached scan may carry an old max if a check was added/removed
1962 // between releases (see 2.6.1: closed_plugins raised internal from 22 to 28
1963 // but cached scans still reported max=22 until reset).
1964 $cat_max = (int) $meta['max'];
1965 $info_only = ! empty( $meta['info_only'] ) || 0 === (int) $meta['max'];
1966 $cat_pct = $cat_max > 0 ? (int) round( ( $earned / $cat_max ) * 100 ) : 0;
1967 $checks = isset( $cat['checks'] ) ? (array) $cat['checks'] : array();
1968 $cat_counts = isset( $cat['counts'] ) && is_array( $cat['counts'] ) ? $cat['counts'] : array( 'pass' => 0, 'warn' => 0, 'fail' => 0, 'info' => 0 );
1969 $cat_quality = self::analyzer_quality_tag( $cat_pct );
1970 $info_count = isset( $cat_counts['info'] ) ? (int) $cat_counts['info'] : 0;
1971 ?>
1972 <details class="vigilante-analyzer-category<?php echo $info_only ? ' vigilante-analyzer-category--info' : ''; ?>"
1973 data-category="<?php echo esc_attr( $slug ); ?>"
1974 data-info-only="<?php echo $info_only ? '1' : '0'; ?>">
1975 <summary class="vigilante-analyzer-category-summary">
1976 <span class="vigilante-analyzer-category-chevron" aria-hidden="true"></span>
1977 <span class="vigilante-analyzer-category-label"><?php echo esc_html( $meta['label'] ); ?></span>
1978 <?php if ( $info_only ) : ?>
1979 <span class="vigilante-analyzer-category-quality vigilante-analyzer-quality-info"
1980 data-role="category-quality"
1981 title="<?php esc_attr_e( 'Informational — does not affect the security score.', 'vigilante' ); ?>">
1982 <span class="dashicons dashicons-info-outline" aria-hidden="true"></span>
1983 <?php esc_html_e( 'Informational', 'vigilante' ); ?>
1984 </span>
1985 <?php else :
1986 $passed_count = (int) ( $cat_counts['pass'] ?? 0 );
1987 $scored_total = $passed_count
1988 + (int) ( $cat_counts['warn'] ?? 0 )
1989 + (int) ( $cat_counts['fail'] ?? 0 );
1990 ?>
1991 <span class="vigilante-analyzer-category-quality vigilante-analyzer-quality-<?php echo esc_attr( $cat_quality['slug'] ); ?>"
1992 data-role="category-quality">
1993 <span data-role="category-quality-label"><?php echo esc_html( $cat_quality['label'] ); ?></span>
1994 <span class="vigilante-analyzer-category-quality-sep" aria-hidden="true">·</span>
1995 <span class="vigilante-analyzer-category-tests" data-role="category-tests"
1996 data-passed="<?php echo esc_attr( $passed_count ); ?>"
1997 data-total="<?php echo esc_attr( $scored_total ); ?>">
1998 <?php
1999 echo esc_html( sprintf(
2000 /* translators: 1: tests passed, 2: total tests in this category */
2001 __( '%1$d/%2$d tests', 'vigilante' ),
2002 $passed_count,
2003 $scored_total
2004 ) );
2005 ?>
2006 </span>
2007 </span>
2008 <?php endif; ?>
2009 <?php if ( $info_only ) : ?>
2010 <span class="vigilante-analyzer-category-states" data-role="category-states">
2011 <?php if ( $info_count > 0 ) : ?>
2012 <span class="vigilante-analyzer-category-state vigilante-analyzer-category-state--info" title="<?php esc_attr_e( 'Informational', 'vigilante' ); ?>">
2013 <span data-role="state-info"><?php echo esc_html( $info_count ); ?></span><span class="dashicons dashicons-info-outline" aria-hidden="true"></span>
2014 </span>
2015 <?php endif; ?>
2016 </span>
2017 <?php endif; ?>
2018 <?php if ( ! $info_only ) : ?>
2019 <span class="vigilante-analyzer-category-score">
2020 <span data-role="earned"><?php echo esc_html( $earned ); ?></span><span class="vigilante-analyzer-category-score-sep">/</span><?php echo esc_html( $cat_max ); ?>
2021 <span class="vigilante-analyzer-category-score-unit"><?php esc_html_e( 'pts', 'vigilante' ); ?></span>
2022 </span>
2023 <span class="vigilante-analyzer-category-bar" aria-hidden="true">
2024 <span class="vigilante-analyzer-category-bar-fill vigilante-analyzer-category-bar-fill--<?php echo esc_attr( $cat_quality['slug'] ); ?>"
2025 style="width: <?php echo esc_attr( $cat_pct ); ?>%"
2026 data-role="category-bar"></span>
2027 </span>
2028 <?php else :
2029 $info_warn = isset( $cat_counts['warn'] ) ? (int) $cat_counts['warn'] : 0;
2030 $info_fail = isset( $cat_counts['fail'] ) ? (int) $cat_counts['fail'] : 0;
2031 $info_issues = $info_warn + $info_fail;
2032 if ( $info_issues > 0 ) : ?>
2033 <span class="vigilante-analyzer-category-status vigilante-analyzer-category-status--attention" data-role="info-status">
2034 <span class="dashicons dashicons-warning" aria-hidden="true"></span>
2035 <?php
2036 echo esc_html( sprintf(
2037 /* translators: %d: number of findings */
2038 _n( '%d finding', '%d findings', $info_issues, 'vigilante' ),
2039 $info_issues
2040 ) );
2041 ?>
2042 </span>
2043 <?php else : ?>
2044 <span class="vigilante-analyzer-category-status vigilante-analyzer-category-status--clear" data-role="info-status">
2045 <span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>
2046 <?php esc_html_e( 'All clear', 'vigilante' ); ?>
2047 </span>
2048 <?php endif; ?>
2049 <?php endif; ?>
2050 </summary>
2051 <ul class="vigilante-analyzer-check-list" data-role="check-list">
2052 <?php if ( empty( $checks ) ) : ?>
2053 <li class="vigilante-analyzer-check-empty">
2054 <?php esc_html_e( 'No data yet — run a scan to populate this category.', 'vigilante' ); ?>
2055 </li>
2056 <?php else : ?>
2057 <?php foreach ( $checks as $c ) : ?>
2058 <?php echo self::render_analyzer_check_row( $c ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped inside helper ?>
2059 <?php endforeach; ?>
2060 <?php endif; ?>
2061 </ul>
2062 </details>
2063 <?php endforeach; ?>
2064 </div>
2065
2066 <div class="vigilante-analyzer-weekly">
2067 <h3><?php esc_html_e( 'Automatic weekly scan', 'vigilante' ); ?></h3>
2068 <p class="description">
2069 <?php esc_html_e( 'Vigilante runs this check once a week in the background. Enable email alerts to be notified if the score drops by 10 points or more, or if a new critical check starts failing.', 'vigilante' ); ?>
2070 </p>
2071 <label class="vigilante-analyzer-toggle-option">
2072 <input type="checkbox"
2073 name="security_analyzer[weekly_scan_enabled]"
2074 value="1"
2075 <?php checked( $weekly_enabled ); ?>>
2076 <?php esc_html_e( 'Run a weekly automatic scan', 'vigilante' ); ?>
2077 </label>
2078 <label class="vigilante-analyzer-toggle-option">
2079 <input type="checkbox"
2080 name="security_analyzer[email_on_regression]"
2081 value="1"
2082 <?php checked( $email_enabled ); ?>>
2083 <?php esc_html_e( 'Email me when the score drops significantly', 'vigilante' ); ?>
2084 </label>
2085 </div>
2086 </div>
2087 </div>
2088 <?php
2089 }
2090
2091 /**
2092 * Map a 0-100 percentage to a quality tag { label, slug } aligned with the
2093 * Dashboard grade palette (a/b/c/d/e).
2094 *
2095 * @param int $pct 0..100.
2096 * @return array{label:string,slug:string}
2097 */
2098 public static function analyzer_quality_tag( $pct ) {
2099 $pct = max( 0, min( 100, (int) $pct ) );
2100 // "Excellent" is reserved for a perfect score — a single missing point drops to Good.
2101 if ( 100 === $pct ) {
2102 return array( 'label' => __( 'Excellent', 'vigilante' ), 'slug' => 'a' );
2103 }
2104 if ( $pct >= 70 ) {
2105 return array( 'label' => __( 'Good', 'vigilante' ), 'slug' => 'b' );
2106 }
2107 if ( $pct >= 50 ) {
2108 return array( 'label' => __( 'Fair', 'vigilante' ), 'slug' => 'c' );
2109 }
2110 if ( $pct >= 30 ) {
2111 return array( 'label' => __( 'Poor', 'vigilante' ), 'slug' => 'd' );
2112 }
2113 return array( 'label' => __( 'Critical', 'vigilante' ), 'slug' => 'e' );
2114 }
2115
2116 /**
2117 * Render a single analyzer check row (used both server-side and via JS template).
2118 *
2119 * @param array $check Check result array (from Vigilante_SA_Check_Result::to_array()).
2120 * @return string HTML (escaped).
2121 */
2122 private static function render_analyzer_check_row( $check ) {
2123 $id = isset( $check['id'] ) ? $check['id'] : '';
2124 $state = isset( $check['state'] ) ? $check['state'] : 'skip';
2125 $label = isset( $check['label'] ) ? $check['label'] : '';
2126 $detail = isset( $check['detail'] ) ? $check['detail'] : '';
2127 $score = isset( $check['score'] ) ? (int) $check['score'] : 0;
2128 $max = isset( $check['max'] ) ? (int) $check['max'] : 0;
2129 $fix_link = isset( $check['fix_link'] ) ? $check['fix_link'] : '';
2130
2131 $icons = array(
2132 'pass' => 'yes-alt',
2133 'warn' => 'warning',
2134 'fail' => 'dismiss',
2135 'info' => 'info',
2136 'skip' => 'minus',
2137 );
2138 $icon = isset( $icons[ $state ] ) ? $icons[ $state ] : 'minus';
2139
2140 $html = '<li class="vigilante-analyzer-check vigilante-analyzer-check--' . esc_attr( $state ) . '"';
2141 $html .= ' data-check-id="' . esc_attr( $id ) . '">';
2142 $html .= '<span class="vigilante-analyzer-check-icon dashicons dashicons-' . esc_attr( $icon ) . '" aria-hidden="true"></span>';
2143 $html .= '<div class="vigilante-analyzer-check-body">';
2144 $html .= '<div class="vigilante-analyzer-check-label">';
2145 $html .= '<span>' . esc_html( $label ) . '</span>';
2146 if ( $max > 0 && 'info' !== $state && 'skip' !== $state ) {
2147 $html .= '<span class="vigilante-analyzer-check-score">'
2148 . esc_html( $score . '/' . $max )
2149 . ' <span class="vigilante-analyzer-check-score-unit">' . esc_html__( 'pts', 'vigilante' ) . '</span>'
2150 . '</span>';
2151 }
2152 $html .= '</div>';
2153 if ( $detail ) {
2154 $html .= '<p class="vigilante-analyzer-check-detail">' . esc_html( $detail ) . '</p>';
2155 }
2156 if ( $fix_link && in_array( $state, array( 'fail', 'warn' ), true ) ) {
2157 $html .= '<a href="' . esc_url( $fix_link ) . '" class="vigilante-analyzer-fix-link">'
2158 . esc_html__( 'Go to setting', 'vigilante' )
2159 . '<span class="vigilante-analyzer-fix-arrow" aria-hidden="true">&rarr;</span></a>';
2160 } elseif ( $fix_link && 'info' === $state ) {
2161 // Info rows (e.g. DNSBL lookups) get an external "Learn more" link instead.
2162 $is_external = 0 === strpos( $fix_link, 'http' );
2163 $html .= '<a href="' . esc_url( $fix_link ) . '" class="vigilante-analyzer-fix-link"'
2164 . ( $is_external ? ' target="_blank" rel="noopener noreferrer"' : '' ) . '>'
2165 . esc_html__( 'Learn more', 'vigilante' )
2166 . '<span class="vigilante-analyzer-fix-arrow" aria-hidden="true">&rarr;</span></a>';
2167 }
2168 $html .= '</div>';
2169 $html .= '</li>';
2170 return $html;
2171 }
2172
2173 /**
2174 * Build a minimal SVG sparkline for the score history.
2175 *
2176 * @param int[] $points Score values (0..100), oldest to newest.
2177 * @return string SVG markup.
2178 */
2179 private static function sparkline_svg( $points ) {
2180 $points = array_map( 'intval', (array) $points );
2181 $count = count( $points );
2182 if ( $count < 2 ) {
2183 return '';
2184 }
2185 $width = 280;
2186 $height = 60;
2187 $padding = 4;
2188
2189 $usable_w = $width - ( $padding * 2 );
2190 $usable_h = $height - ( $padding * 2 );
2191
2192 $step = $usable_w / max( 1, $count - 1 );
2193 $max = 100; // Fixed scale — scores are 0..100.
2194
2195 $coords = array();
2196 foreach ( $points as $i => $v ) {
2197 $x = $padding + ( $i * $step );
2198 $y = $padding + ( $usable_h - ( ( $v / $max ) * $usable_h ) );
2199 $coords[] = round( $x, 2 ) . ',' . round( $y, 2 );
2200 }
2201 $path = 'M ' . implode( ' L ', $coords );
2202 $last = end( $points );
2203 $last_x = $padding + ( ( $count - 1 ) * $step );
2204 $last_y = $padding + ( $usable_h - ( ( $last / $max ) * $usable_h ) );
2205
2206 $svg = '<svg viewBox="0 0 ' . $width . ' ' . $height . '" preserveAspectRatio="none" role="img" aria-label="' . esc_attr__( 'Security Score history', 'vigilante' ) . '" focusable="false">';
2207 $svg .= '<path d="' . esc_attr( $path ) . '" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
2208 $svg .= '<circle cx="' . esc_attr( round( $last_x, 2 ) ) . '" cy="' . esc_attr( round( $last_y, 2 ) ) . '" r="3" fill="currentColor"/>';
2209 $svg .= '</svg>';
2210 return $svg;
2211 }
2212
2213 /**
2214 * Render dashboard tab
2215 */
2216 private function render_tab_dashboard() {
2217 $options = $this->settings->get_all_options();
2218 $module_labels = $this->settings->get_module_labels();
2219 $module_descriptions = $this->settings->get_module_descriptions();
2220 $presets = $this->settings->get_presets();
2221 $active_preset = get_option( 'vigilante_active_preset', '' );
2222
2223 // Calculate security score with more factors
2224 $security_score = $this->calculate_security_score( $options );
2225
2226 // Security Analyzer (v2.1.0) — hydrate widget with the last persisted scan, if any.
2227 if ( ! class_exists( 'Vigilante_Security_Analyzer' ) ) {
2228 require_once VIGILANTE_INCLUDES_DIR . 'class-security-analyzer.php';
2229 }
2230 $analyzer_instance = new Vigilante_Security_Analyzer( $this->settings, $this->activity_log );
2231 $analyzer_last_scan = $analyzer_instance->get_last_scan();
2232 $analyzer_history = $analyzer_instance->get_score_history();
2233 $analyzer_categories_def = Vigilante_Security_Analyzer::get_categories();
2234 $analyzer_settings = isset( $options['security_analyzer'] ) ? $options['security_analyzer'] : array();
2235 ?>
2236 <div class="vigilante-dashboard">
2237 <div class="vigilante-status-card">
2238 <h2><?php esc_html_e( 'Configuration Score', 'vigilante' ); ?></h2>
2239 <p class="vigilante-score-kind description">
2240 <?php esc_html_e( 'How well Vigilante is configured right now. Pair it with the Security Check below to see the real-world result.', 'vigilante' ); ?>
2241 </p>
2242 <div class="vigilante-security-score">
2243 <?php
2244 // Grade thresholds: A (90+), B (70-89), C (50-69), D (30-49), E (0-29)
2245 if ( $security_score >= 90 ) {
2246 $grade = 'A';
2247 } elseif ( $security_score >= 70 ) {
2248 $grade = 'B';
2249 } elseif ( $security_score >= 50 ) {
2250 $grade = 'C';
2251 } elseif ( $security_score >= 30 ) {
2252 $grade = 'D';
2253 } else {
2254 $grade = 'E';
2255 }
2256 ?>
2257 <div class="vigilante-score-circle vigilante-grade-<?php echo esc_attr( strtolower( $grade ) ); ?>">
2258 <span class="vigilante-grade"><?php echo esc_html( $grade ); ?></span>
2259 <span class="vigilante-score-text"><?php echo esc_html( $security_score ); ?>%</span>
2260 </div>
2261 <div class="vigilante-config-status">
2262 <?php if ( $active_preset && isset( $presets[ $active_preset ] ) ) : ?>
2263 <span class="vigilante-preset-badge vigilante-preset-<?php echo esc_attr( $active_preset ); ?>">
2264 <?php echo esc_html( $presets[ $active_preset ]['name'] ); ?>
2265 </span>
2266 <?php else : ?>
2267 <span class="vigilante-preset-badge vigilante-preset-custom">
2268 <?php esc_html_e( 'Custom Configuration', 'vigilante' ); ?>
2269 </span>
2270 <?php endif; ?>
2271 </div>
2272 </div>
2273
2274 <?php
2275 $recommendations = $this->get_security_recommendations( $options );
2276 if ( ! empty( $recommendations ) ) :
2277 ?>
2278 <div class="vigilante-recommendations">
2279 <h4><?php esc_html_e( 'Recommendations', 'vigilante' ); ?></h4>
2280 <ul class="vigilante-recommendations-grid">
2281 <?php foreach ( $recommendations as $rec ) : ?>
2282 <li>
2283 <span class="dashicons dashicons-<?php echo esc_attr( $rec['icon'] ); ?> vigilante-priority-<?php echo esc_attr( $rec['priority'] ); ?>"></span>
2284 <?php echo esc_html( $rec['message'] ); ?>
2285 <?php if ( ! empty( $rec['tab'] ) ) : ?>
2286 <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=' . $rec['tab'] ) ); ?>" class="vigilante-rec-link" title="<?php esc_attr_e( 'Go to settings', 'vigilante' ); ?>"><span class="dashicons dashicons-arrow-right-alt2"></span></a>
2287 <?php endif; ?>
2288 </li>
2289 <?php endforeach; ?>
2290 </ul>
2291 </div>
2292 <?php endif; ?>
2293 </div>
2294
2295 <?php $this->render_analyzer_widget( $analyzer_last_scan, $analyzer_history, $analyzer_categories_def, $analyzer_settings ); ?>
2296
2297 <div class="vigilante-modules-grid">
2298 <h2><?php esc_html_e( 'Security Modules', 'vigilante' ); ?></h2>
2299 <p class="description"><?php esc_html_e( 'Enable or disable security modules. Each module controls a tab with detailed settings.', 'vigilante' ); ?></p>
2300 <div class="vigilante-modules-list">
2301 <?php foreach ( $options['modules'] as $module => $enabled ) :
2302 $label = isset( $module_labels[ $module ] ) ? $module_labels[ $module ] : ucwords( str_replace( '_', ' ', $module ) );
2303 $description = isset( $module_descriptions[ $module ] ) ? $module_descriptions[ $module ] : '';
2304 ?>
2305 <div class="vigilante-module-item <?php echo $enabled ? 'enabled' : 'disabled'; ?>">
2306 <div class="vigilante-module-header">
2307 <span class="vigilante-module-status"></span>
2308 <span class="vigilante-module-name"><?php echo esc_html( $label ); ?></span>
2309 <label class="vigilante-toggle">
2310 <?php
2311 /* translators: %s: Security module name, for example Firewall. */
2312 $toggle_label = sprintf( __( 'Enable %s', 'vigilante' ), $label );
2313 ?>
2314 <input type="checkbox"
2315 name="modules[<?php echo esc_attr( $module ); ?>]"
2316 value="1"
2317 <?php checked( $enabled ); ?>
2318 aria-label="<?php echo esc_attr( $toggle_label ); ?>"
2319 data-module="<?php echo esc_attr( $module ); ?>">
2320 <span class="vigilante-toggle-slider"></span>
2321 </label>
2322 </div>
2323 <?php if ( $description ) : ?>
2324 <p class="vigilante-module-desc"><?php echo esc_html( $description ); ?></p>
2325 <?php endif; ?>
2326 </div>
2327 <?php endforeach; ?>
2328 </div>
2329 </div>
2330
2331 <div class="vigilante-presets-card">
2332 <h2><?php esc_html_e( 'Quick Configuration Presets', 'vigilante' ); ?></h2>
2333 <p><?php esc_html_e( 'Apply a preset to quickly set up recommended settings for standard or maximum security level.', 'vigilante' ); ?></p>
2334 <div class="vigilante-presets-grid">
2335 <?php foreach ( $presets as $preset_id => $preset ) :
2336 $is_active = ( $active_preset === $preset_id );
2337 ?>
2338 <div class="vigilante-preset-card <?php echo $is_active ? 'vigilante-preset-active' : ''; ?>">
2339 <?php if ( $is_active ) : ?>
2340 <span class="vigilante-active-indicator"><?php esc_html_e( 'Active', 'vigilante' ); ?></span>
2341 <?php endif; ?>
2342 <h3><?php echo esc_html( $preset['name'] ); ?></h3>
2343 <p><?php echo esc_html( $preset['description'] ); ?></p>
2344 <button type="button" class="button vigilante-preset-btn <?php echo $is_active ? 'button-primary' : ''; ?>" data-preset="<?php echo esc_attr( $preset_id ); ?>">
2345 <?php esc_html_e( 'Apply Preset', 'vigilante' ); ?>
2346 </button>
2347 </div>
2348 <?php endforeach; ?>
2349
2350 <?php
2351 // Under Attack mode card
2352 $under_attack = new Vigilante_Under_Attack( $this->settings, $this->activity_log );
2353 $ua_active = $under_attack->is_active();
2354 $ua_remaining = $under_attack->get_remaining_time();
2355 $ua_remaining_hours = floor( $ua_remaining / 3600 );
2356 $ua_remaining_mins = floor( ( $ua_remaining % 3600 ) / 60 );
2357 ?>
2358 <div class="vigilante-preset-card vigilante-under-attack-card <?php echo $ua_active ? 'vigilante-under-attack-active' : ''; ?>">
2359 <h3>
2360 <span class="dashicons dashicons-shield"></span>
2361 <?php esc_html_e( 'Under Attack', 'vigilante' ); ?>
2362 </h3>
2363 <p><?php esc_html_e( 'Emergency mode. JavaScript challenge for all visitors, aggressive rate limiting, and restricted access. Auto-deactivates after 4 hours.', 'vigilante' ); ?></p>
2364 <?php if ( $ua_active ) : ?>
2365 <div class="vigilante-ua-countdown" data-expires="<?php echo esc_attr( $under_attack->get_status()['activated_at'] + $under_attack->get_status()['duration'] ); ?>">
2366 <span class="dashicons dashicons-clock"></span>
2367 <span class="vigilante-ua-time">
2368 <?php
2369 printf(
2370 /* translators: 1: Hours, 2: Minutes */
2371 esc_html__( '%1$dh %2$dm remaining', 'vigilante' ),
2372 absint( $ua_remaining_hours ),
2373 absint( $ua_remaining_mins )
2374 );
2375 ?>
2376 </span>
2377 </div>
2378 <button type="button" class="button vigilante-ua-btn vigilante-ua-deactivate">
2379 <?php esc_html_e( 'Deactivate', 'vigilante' ); ?>
2380 </button>
2381 <?php else : ?>
2382 <button type="button" class="button vigilante-ua-btn vigilante-ua-activate">
2383 <?php esc_html_e( 'Activate for 4 hours', 'vigilante' ); ?>
2384 </button>
2385 <?php endif; ?>
2386 </div>
2387 </div>
2388 </div>
2389 </div>
2390 <?php
2391 }
2392
2393 /**
2394 * Render tools tab
2395 */
2396 private function render_tab_tools() {
2397 // Get current settings for notification summary
2398 $email_options = $this->settings->get_section( 'email' );
2399 $login_options = $this->settings->get_section( 'login_security' );
2400 $user_options = $this->settings->get_section( 'user_security' );
2401 $fi_options = $this->settings->get_section( 'file_integrity' );
2402 $alerts_options = $this->settings->get_section( 'audit_alerts' );
2403 $monitoring = $user_options['admin_monitoring'] ?? array();
2404 $registration = $user_options['registration_approval'] ?? array();
2405 $current_admin = get_option( 'admin_email' );
2406 $send_to_admin = ! isset( $email_options['send_to_admin_email'] ) || ! empty( $email_options['send_to_admin_email'] );
2407 $additional_raw = $email_options['additional_recipients'] ?? array();
2408 $additional = is_array( $additional_raw ) ? implode( "\n", $additional_raw ) : trim( $additional_raw );
2409 ?>
2410
2411 <!-- Notification Settings -->
2412 <div id="vigilante-section-tools-notifications" class="vigilante-settings-section vigilante-notification-section">
2413 <h2><?php esc_html_e( 'Notification settings', 'vigilante' ); ?></h2>
2414 <p><?php esc_html_e( 'Configure who receives all administrative email notifications from Vigilant. Individual notifications are enabled in their respective tabs.', 'vigilante' ); ?></p>
2415
2416 <div class="vigilante-notification-layout">
2417
2418 <!-- Left column: Recipients settings -->
2419 <div class="vigilante-notification-settings">
2420 <form class="vigilante-settings-form" data-section="email">
2421
2422 <table class="form-table vigilante-compact-form">
2423 <tr>
2424 <th scope="row"><?php esc_html_e( 'WordPress Admin Email', 'vigilante' ); ?></th>
2425 <td>
2426 <label>
2427 <input type="checkbox" name="email[send_to_admin_email]" value="1" <?php checked( $send_to_admin ); ?>>
2428 <?php
2429 printf(
2430 /* translators: %s: Admin email address */
2431 esc_html__( 'Send to admin email (%s)', 'vigilante' ),
2432 '<code>' . esc_html( $current_admin ) . '</code>'
2433 );
2434 ?>
2435 </label>
2436 </td>
2437 </tr>
2438 <tr>
2439 <th scope="row"><?php esc_html_e( 'Additional Recipients', 'vigilante' ); ?></th>
2440 <td>
2441 <textarea name="email[additional_recipients]" rows="3" class="large-text code" placeholder="maintenance@example.com&#10;security@example.com"><?php echo esc_textarea( $additional ); ?></textarea>
2442 <p class="description"><?php esc_html_e( 'One email per line.', 'vigilante' ); ?></p>
2443 </td>
2444 </tr>
2445 <tr>
2446 <th scope="row"><?php esc_html_e( 'Plugin Deactivation', 'vigilante' ); ?></th>
2447 <td>
2448 <label>
2449 <input type="checkbox" name="email[send_deactivation_email]" value="1" <?php checked( ! empty( $email_options['send_deactivation_email'] ) ); ?>>
2450 <?php esc_html_e( 'Send email when Vigilant is deactivated', 'vigilante' ); ?>
2451 </label>
2452 </td>
2453 </tr>
2454 </table>
2455
2456 <p class="submit vigilante-notification-submit">
2457 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
2458 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
2459 </button>
2460 <button type="button" class="button vigilante-test-email-btn" data-original-text="<?php esc_attr_e( 'Send test email', 'vigilante' ); ?>">
2461 <?php esc_html_e( 'Send test email', 'vigilante' ); ?>
2462 </button>
2463 <span class="vigilante-test-email-result" style="margin-left:8px;vertical-align:middle;"></span>
2464 </p>
2465 <p class="description"><?php esc_html_e( 'The test goes to the recipients above and confirms that email delivery works for every Vigilant notification.', 'vigilante' ); ?></p>
2466
2467 </form>
2468 </div>
2469
2470 <!-- Right column: Active notifications summary -->
2471 <div class="vigilante-notification-summary">
2472 <h4><?php esc_html_e( 'Active notifications', 'vigilante' ); ?></h4>
2473
2474 <table class="widefat striped">
2475 <thead>
2476 <tr>
2477 <th><?php esc_html_e( 'Notification', 'vigilante' ); ?></th>
2478 <th style="width: 1%; white-space: nowrap; text-align: center;"><?php esc_html_e( 'Status', 'vigilante' ); ?></th>
2479 <th style="width: 50px; text-align: center;"></th>
2480 </tr>
2481 </thead>
2482 <tbody>
2483 <?php
2484 $notifications = array(
2485 array(
2486 'label' => __( 'Login lockout', 'vigilante' ),
2487 'active' => ! empty( $login_options['notify_on_lockout'] ),
2488 'tab' => 'login',
2489 ),
2490 array(
2491 'label' => __( 'Administrator login', 'vigilante' ),
2492 'active' => ! empty( $login_options['notify_on_admin_login'] ),
2493 'tab' => 'login',
2494 ),
2495 array(
2496 'label' => __( 'New administrator created', 'vigilante' ),
2497 'active' => ! empty( $monitoring['alert_new_admin'] ),
2498 'tab' => 'users',
2499 ),
2500 array(
2501 'label' => __( 'Administrator email changed', 'vigilante' ),
2502 'active' => ! empty( $monitoring['alert_admin_email_change'] ),
2503 'tab' => 'users',
2504 ),
2505 array(
2506 'label' => __( 'Permission elevation', 'vigilante' ),
2507 'active' => ! empty( $monitoring['alert_permission_elevation'] ),
2508 'tab' => 'users',
2509 ),
2510 array(
2511 'label' => __( 'Admin password changed', 'vigilante' ),
2512 'active' => ! empty( $monitoring['alert_admin_password_change'] ),
2513 'tab' => 'users',
2514 ),
2515 array(
2516 'label' => __( 'Registration pending approval', 'vigilante' ),
2517 'active' => ! empty( $registration['enabled'] ) && ! empty( $registration['notify_admin'] ),
2518 'tab' => 'users',
2519 ),
2520 array(
2521 'label' => __( 'File integrity scan report', 'vigilante' ),
2522 'active' => ( $fi_options['notify_level'] ?? 'disabled' ) !== 'disabled',
2523 'tab' => 'file-integrity',
2524 ),
2525 array(
2526 'label' => __( 'File integrity instant alert', 'vigilante' ),
2527 'active' => ! empty( $fi_options['instant_alert'] ),
2528 'tab' => 'file-integrity',
2529 ),
2530 array(
2531 'label' => __( 'Audit alert: immediate', 'vigilante' ),
2532 'active' => Vigilante_Audit_Alerts::immediate_is_active( $alerts_options ),
2533 'tab' => 'activity-log',
2534 'anchor' => 'vigilante-section-audit-alerts',
2535 ),
2536 array(
2537 'label' => __( 'Audit alert: threshold', 'vigilante' ),
2538 'active' => Vigilante_Audit_Alerts::threshold_is_active( $alerts_options ),
2539 'tab' => 'activity-log',
2540 'anchor' => 'vigilante-section-audit-alerts',
2541 ),
2542 array(
2543 'label' => __( 'Under Attack mode', 'vigilante' ),
2544 'active' => true,
2545 'tab' => '',
2546 'note' => __( 'Always active', 'vigilante' ),
2547 ),
2548 array(
2549 'label' => __( 'Plugin deactivation', 'vigilante' ),
2550 'active' => ! empty( $email_options['send_deactivation_email'] ),
2551 'tab' => 'tools',
2552 ),
2553 );
2554
2555 foreach ( $notifications as $notif ) :
2556 $status_class = $notif['active'] ? 'vigilante-status-active' : 'vigilante-status-inactive';
2557 $status_label = $notif['active'] ? __( 'Active', 'vigilante' ) : __( 'Inactive', 'vigilante' );
2558 if ( ! empty( $notif['note'] ) ) {
2559 $status_label = $notif['note'];
2560 }
2561 ?>
2562 <tr>
2563 <td><?php echo esc_html( $notif['label'] ); ?></td>
2564 <td style="text-align: center; white-space: nowrap;">
2565 <span class="<?php echo esc_attr( $status_class ); ?>"><?php echo esc_html( $status_label ); ?></span>
2566 </td>
2567 <td style="text-align: center;">
2568 <?php if ( ! empty( $notif['tab'] ) ) : ?>
2569 <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=' . $notif['tab'] ) . ( ! empty( $notif['anchor'] ) ? '#' . $notif['anchor'] : '' ) ); ?>" class="button button-small">
2570 <span class="dashicons dashicons-admin-generic" style="font-size: 14px; line-height: 1.8;"></span>
2571 </a>
2572 <?php else : ?>
2573 &mdash;
2574 <?php endif; ?>
2575 </td>
2576 </tr>
2577 <?php endforeach; ?>
2578 </tbody>
2579 </table>
2580 </div>
2581
2582 </div>
2583 </div>
2584
2585 <h2 id="vigilante-section-tools-main" class="vigilante-tools-heading"><?php esc_html_e( 'Tools', 'vigilante' ); ?></h2>
2586
2587 <?php
2588 // Warn that during Under Attack mode the export/import operate against
2589 // the temporary hardened config and any imported changes will be
2590 // reverted when the mode ends.
2591 $ua_status = get_option( Vigilante_Under_Attack::OPTION_NAME, array() );
2592 if ( ! empty( $ua_status['active'] ) ) :
2593 ?>
2594 <div class="vigilante-ua-tools-notice-wrap">
2595 <div class="notice notice-warning inline vigilante-ua-tools-notice">
2596 <p>
2597 <strong><?php esc_html_e( 'Under Attack mode is active.', 'vigilante' ); ?></strong>
2598 <?php esc_html_e( 'Exports will reflect the temporary hardened configuration, not your saved one. Imports will apply on top of the hardened config and will be reverted when the mode ends. Consider waiting until you deactivate Under Attack before exporting or importing settings.', 'vigilante' ); ?>
2599 </p>
2600 </div>
2601 </div>
2602 <?php
2603 endif;
2604 ?>
2605
2606 <div class="vigilante-tools-grid">
2607 <div class="vigilante-tool-card">
2608 <h3><?php esc_html_e( 'Export Settings', 'vigilante' ); ?></h3>
2609 <p><?php esc_html_e( 'Download your current security settings as a JSON file.', 'vigilante' ); ?></p>
2610 <button type="button" class="button vigilante-export-settings">
2611 <?php esc_html_e( 'Export Settings', 'vigilante' ); ?>
2612 </button>
2613 </div>
2614
2615 <div class="vigilante-tool-card">
2616 <h3><?php esc_html_e( 'Import Settings', 'vigilante' ); ?></h3>
2617 <p><?php esc_html_e( 'Import settings from a previously exported JSON file.', 'vigilante' ); ?></p>
2618 <input type="file" id="vigilante-import-file" accept=".json" style="display: none;">
2619 <button type="button" class="button vigilante-import-settings">
2620 <?php esc_html_e( 'Import Settings', 'vigilante' ); ?>
2621 </button>
2622 </div>
2623
2624 <div class="vigilante-tool-card">
2625 <h3><?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?></h3>
2626 <p><?php esc_html_e( 'Reset all the Vigilant security settings to default values.', 'vigilante' ); ?></p>
2627 <button type="button" class="button vigilante-reset-settings" style="color: #a00;">
2628 <?php esc_html_e( 'Reset All Settings', 'vigilante' ); ?>
2629 </button>
2630 </div>
2631
2632 <div class="vigilante-tool-card">
2633 <h3><?php esc_html_e( 'Download Config Backup', 'vigilante' ); ?></h3>
2634 <p><?php esc_html_e( 'Download a ZIP backup of your wp-config.php and .htaccess (plus robots.txt if present) before making security changes. The archive is built on the fly and sent to your browser, so nothing is left on the server.', 'vigilante' ); ?></p>
2635 <button type="button" class="button vigilante-create-backup">
2636 <?php esc_html_e( 'Download Backup', 'vigilante' ); ?>
2637 </button>
2638 </div>
2639
2640 <div class="vigilante-tool-card vigilante-tool-card-wide">
2641 <h3><?php esc_html_e( 'Database Backup', 'vigilante' ); ?></h3>
2642 <p><?php esc_html_e( 'Download a backup of your database as a ZIP file. Select which tables to include.', 'vigilante' ); ?></p>
2643 <button type="button" class="button vigilante-db-backup-toggle">
2644 <?php esc_html_e( 'Download Database Backup', 'vigilante' ); ?>
2645 </button>
2646
2647 <div class="vigilante-db-backup-panel" style="display: none;">
2648 <div class="vigilante-db-tables-loading">
2649 <span class="spinner is-active"></span>
2650 <?php esc_html_e( 'Loading tables...', 'vigilante' ); ?>
2651 </div>
2652
2653 <div class="vigilante-db-tables-content" style="display: none;">
2654 <div class="vigilante-db-tables-controls">
2655 <label>
2656 <input type="checkbox" id="vigilante-db-select-all" checked>
2657 <strong><?php esc_html_e( 'Select / deselect all', 'vigilante' ); ?></strong>
2658 </label>
2659 <span class="vigilante-db-tables-info"></span>
2660 </div>
2661
2662 <div class="vigilante-db-tables-group">
2663 <h4><?php esc_html_e( 'WordPress core tables', 'vigilante' ); ?></h4>
2664 <div class="vigilante-db-tables-list" id="vigilante-db-core-tables"></div>
2665 </div>
2666
2667 <div class="vigilante-db-tables-group" id="vigilante-db-other-group" style="display: none;">
2668 <h4><?php esc_html_e( 'Plugin and custom tables', 'vigilante' ); ?></h4>
2669 <div class="vigilante-db-tables-list" id="vigilante-db-other-tables"></div>
2670 </div>
2671
2672 <div class="vigilante-db-backup-actions">
2673 <button type="button" class="button button-primary vigilante-db-backup-download">
2674 <?php esc_html_e( 'Download Backup (.zip)', 'vigilante' ); ?>
2675 </button>
2676 </div>
2677 </div>
2678 </div>
2679 </div>
2680 </div>
2681 <?php
2682 }
2683
2684 /**
2685 * Render firewall tab
2686 */
2687 private function render_tab_firewall() {
2688 $is_disabled = $this->render_module_disabled_notice( 'firewall' );
2689 $options = $this->settings->get_section( 'firewall' );
2690 ?>
2691 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="firewall" <?php echo $is_disabled ? 'inert' : ''; ?>>
2692 <div id="vigilante-section-firewall-main" class="vigilante-settings-section">
2693 <h2>
2694 <?php esc_html_e( 'Firewall Protection', 'vigilante' ); ?>
2695 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
2696 </h2>
2697 <p><?php esc_html_e( 'PHP-based request filtering. Analyzes each request before WordPress loads.', 'vigilante' ); ?></p>
2698 <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;">
2699 <p style="margin:0;">
2700 <?php esc_html_e( 'Full page caching systems that serve cached pages before PHP executes (Varnish, LiteSpeed Cache, NGINX FastCGI Cache, Cloudflare APO) may bypass PHP-level firewall rules for cached requests. The .htaccess rules will still apply on Apache/LiteSpeed servers.', 'vigilante' ); ?>
2701 </p>
2702 </div>
2703
2704 <table class="form-table">
2705 <tr>
2706 <th scope="row"><?php esc_html_e( 'Block Bad Query Strings', 'vigilante' ); ?></th>
2707 <td>
2708 <label>
2709 <input type="checkbox" name="firewall[block_bad_query_strings]" value="1" <?php checked( ! empty( $options['block_bad_query_strings'] ) ); ?>>
2710 <?php esc_html_e( 'Block malicious query string patterns', 'vigilante' ); ?>
2711 </label>
2712 </td>
2713 </tr>
2714 <tr>
2715 <th scope="row"><?php esc_html_e( 'SQL Injection Protection', 'vigilante' ); ?></th>
2716 <td>
2717 <label>
2718 <input type="checkbox" name="firewall[block_sql_injection]" value="1" <?php checked( ! empty( $options['block_sql_injection'] ) ); ?>>
2719 <?php esc_html_e( 'Block SQL injection attempts', 'vigilante' ); ?>
2720 </label>
2721 </td>
2722 </tr>
2723 <tr>
2724 <th scope="row"><?php esc_html_e( 'XSS Protection', 'vigilante' ); ?></th>
2725 <td>
2726 <label>
2727 <input type="checkbox" name="firewall[block_xss_attacks]" value="1" <?php checked( ! empty( $options['block_xss_attacks'] ) ); ?>>
2728 <?php esc_html_e( 'Block cross-site scripting attacks', 'vigilante' ); ?>
2729 </label>
2730 </td>
2731 </tr>
2732 <tr>
2733 <th scope="row"><?php esc_html_e( 'File Inclusion Protection', 'vigilante' ); ?></th>
2734 <td>
2735 <label>
2736 <input type="checkbox" name="firewall[block_file_inclusion]" value="1" <?php checked( ! empty( $options['block_file_inclusion'] ) ); ?>>
2737 <?php esc_html_e( 'Block local/remote file inclusion attempts', 'vigilante' ); ?>
2738 </label>
2739 </td>
2740 </tr>
2741 <tr>
2742 <th scope="row"><?php esc_html_e( 'Directory Traversal Protection', 'vigilante' ); ?></th>
2743 <td>
2744 <label>
2745 <input type="checkbox" name="firewall[block_directory_traversal]" value="1" <?php checked( ! empty( $options['block_directory_traversal'] ) ); ?>>
2746 <?php esc_html_e( 'Block path traversal attempts', 'vigilante' ); ?>
2747 </label>
2748 </td>
2749 </tr>
2750 <tr>
2751 <th scope="row"><?php esc_html_e( 'Block Bad Bots', 'vigilante' ); ?></th>
2752 <td>
2753 <label>
2754 <input type="checkbox" name="firewall[block_bad_bots]" value="1" <?php checked( ! empty( $options['block_bad_bots'] ) ); ?>>
2755 <?php esc_html_e( 'Block known malicious bots and scanners', 'vigilante' ); ?>
2756 </label>
2757 </td>
2758 </tr>
2759 </table>
2760
2761 <h3><?php esc_html_e( 'Rate Limiting', 'vigilante' ); ?></h3>
2762 <table class="form-table">
2763 <tr>
2764 <th scope="row"><?php esc_html_e( 'Enable Rate Limiting', 'vigilante' ); ?></th>
2765 <td>
2766 <label>
2767 <input type="checkbox" name="firewall[rate_limiting][enabled]" value="1" <?php checked( ! empty( $options['rate_limiting']['enabled'] ) ); ?>>
2768 <?php esc_html_e( 'Limit requests per IP address', 'vigilante' ); ?>
2769 </label>
2770 </td>
2771 </tr>
2772 <tr>
2773 <th scope="row"><?php esc_html_e( 'Requests per Minute', 'vigilante' ); ?></th>
2774 <td>
2775 <input type="number" name="firewall[rate_limiting][requests_per_minute]" value="<?php echo esc_attr( $options['rate_limiting']['requests_per_minute'] ?? 120 ); ?>" min="10" max="500" class="small-text">
2776 <p class="description">
2777 <?php esc_html_e( 'Counts only PHP requests to WordPress (pages, admin-ajax, REST, login) from a single IP, not static assets like images, CSS or JS. 120/min suits most sites; sustained traffic above that from one IP is usually a bot. To allow a legitimate service, whitelist its IP instead of raising the limit.', 'vigilante' ); ?>
2778 </p>
2779 </td>
2780 </tr>
2781 <tr>
2782 <th scope="row"><?php esc_html_e( 'Block Duration (seconds)', 'vigilante' ); ?></th>
2783 <td>
2784 <input type="number" name="firewall[rate_limiting][block_duration]" value="<?php echo esc_attr( $options['rate_limiting']['block_duration'] ?? 300 ); ?>" min="60" max="3600" class="small-text">
2785 </td>
2786 </tr>
2787 <tr>
2788 <th scope="row"><?php esc_html_e( 'Progressive Blocking', 'vigilante' ); ?></th>
2789 <td>
2790 <label>
2791 <input type="checkbox" name="firewall[rate_limiting][progressive]" value="1" <?php checked( ! empty( $options['rate_limiting']['progressive'] ) ); ?>>
2792 <?php esc_html_e( 'Double block duration on each repeat offense', 'vigilante' ); ?>
2793 </label>
2794 <p class="description">
2795 <?php
2796 $base = absint( $options['rate_limiting']['block_duration'] ?? 300 );
2797 printf(
2798 /* translators: 1: First block duration, 2: Second, 3: Third */
2799 esc_html__( 'Example: %1$s → %2$s → %3$s and so on, up to the maximum.', 'vigilante' ),
2800 esc_html( human_time_diff( 0, $base ) ),
2801 esc_html( human_time_diff( 0, $base * 2 ) ),
2802 esc_html( human_time_diff( 0, $base * 4 ) )
2803 );
2804 ?>
2805 </p>
2806 </td>
2807 </tr>
2808 <tr>
2809 <th scope="row"><?php esc_html_e( 'Maximum Block Duration', 'vigilante' ); ?></th>
2810 <td>
2811 <select name="firewall[rate_limiting][max_block_duration]">
2812 <?php
2813 $max_options = array(
2814 3600 => __( '1 hour', 'vigilante' ),
2815 21600 => __( '6 hours', 'vigilante' ),
2816 43200 => __( '12 hours', 'vigilante' ),
2817 86400 => __( '24 hours', 'vigilante' ),
2818 604800 => __( '7 days', 'vigilante' ),
2819 );
2820 $current_max = absint( $options['rate_limiting']['max_block_duration'] ?? 86400 );
2821 foreach ( $max_options as $val => $label ) :
2822 ?>
2823 <option value="<?php echo esc_attr( $val ); ?>" <?php selected( $current_max, $val ); ?>>
2824 <?php echo esc_html( $label ); ?>
2825 </option>
2826 <?php endforeach; ?>
2827 </select>
2828 <p class="description"><?php esc_html_e( 'Upper limit for progressive blocking.', 'vigilante' ); ?></p>
2829 </td>
2830 </tr>
2831 </table>
2832
2833 <?php
2834 // Currently blocked IPs from rate limiting
2835 $active_blocks = Vigilante_Firewall::get_active_blocks();
2836 if ( ! empty( $active_blocks ) ) :
2837 ?>
2838 <div class="vigilante-settings-section vigilante-lockout-section" style="margin-top:20px;">
2839 <h3><?php esc_html_e( 'Currently Blocked IPs', 'vigilante' ); ?></h3>
2840 <table class="wp-list-table widefat fixed striped" style="max-width:800px;">
2841 <thead>
2842 <tr>
2843 <th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th>
2844 <th><?php esc_html_e( 'Blocked', 'vigilante' ); ?></th>
2845 <th><?php esc_html_e( 'Expires in', 'vigilante' ); ?></th>
2846 <th><?php esc_html_e( 'Strikes', 'vigilante' ); ?></th>
2847 <th><?php esc_html_e( 'Action', 'vigilante' ); ?></th>
2848 </tr>
2849 </thead>
2850 <tbody>
2851 <?php foreach ( $active_blocks as $blocked_ip => $block_data ) : ?>
2852 <tr>
2853 <td><code><?php echo esc_html( $blocked_ip ); ?></code></td>
2854 <td><?php echo esc_html( human_time_diff( $block_data['blocked_at'] ) . ' ' . __( 'ago', 'vigilante' ) ); ?></td>
2855 <td><?php echo esc_html( human_time_diff( time(), $block_data['expires'] ) ); ?></td>
2856 <td><?php echo esc_html( $block_data['strikes'] ?? 1 ); ?></td>
2857 <td>
2858 <button type="button" class="button button-small vigilante-unblock-firewall-ip"
2859 data-ip="<?php echo esc_attr( $blocked_ip ); ?>">
2860 <?php esc_html_e( 'Unblock', 'vigilante' ); ?>
2861 </button>
2862 </td>
2863 </tr>
2864 <?php endforeach; ?>
2865 </tbody>
2866 </table>
2867 </div>
2868 <?php endif; ?>
2869
2870 <h3><?php esc_html_e( 'IP Lists', 'vigilante' ); ?></h3>
2871 <p class="description">
2872 <?php
2873 printf(
2874 /* translators: %s: Current visitor IP address */
2875 esc_html__( 'Your current IP address: %s', 'vigilante' ),
2876 '<code>' . esc_html( $this->database->get_client_ip() ) . '</code>'
2877 );
2878 ?>
2879 </p>
2880 <table class="form-table">
2881 <tr>
2882 <th scope="row"><?php esc_html_e( 'Visitor IP detection', 'vigilante' ); ?></th>
2883 <td>
2884 <?php $proxy_header = $options['trusted_proxy_header'] ?? ''; ?>
2885 <select name="firewall[trusted_proxy_header]">
2886 <option value="" <?php selected( $proxy_header, '' ); ?>><?php esc_html_e( 'Direct connection, only REMOTE_ADDR (recommended)', 'vigilante' ); ?></option>
2887 <option value="cf-connecting-ip" <?php selected( $proxy_header, 'cf-connecting-ip' ); ?>><?php esc_html_e( 'Behind Cloudflare (CF-Connecting-IP)', 'vigilante' ); ?></option>
2888 <option value="x-forwarded-for" <?php selected( $proxy_header, 'x-forwarded-for' ); ?>><?php esc_html_e( 'Behind a reverse proxy or load balancer (X-Forwarded-For)', 'vigilante' ); ?></option>
2889 <option value="x-real-ip" <?php selected( $proxy_header, 'x-real-ip' ); ?>><?php esc_html_e( 'Behind an nginx proxy (X-Real-IP)', 'vigilante' ); ?></option>
2890 </select>
2891 <p class="description">
2892 <?php esc_html_e( 'Where to read the visitor IP from. Leave on "Direct connection" unless your site really sits behind that proxy or CDN. Trusting a forwarded header on a site that is not behind it lets visitors spoof their IP and bypass the IP lists and rate limiting.', 'vigilante' ); ?>
2893 </p>
2894 </td>
2895 </tr>
2896 <tr>
2897 <th scope="row"><?php esc_html_e( 'IP Whitelist', 'vigilante' ); ?></th>
2898 <td>
2899 <textarea name="firewall[ip_whitelist]" rows="4" class="large-text code" placeholder="192.168.1.50&#10;192.168.1.0/24&#10;192.168.1.*"><?php echo esc_textarea( implode( "\n", $options['ip_whitelist'] ?? array() ) ); ?></textarea>
2900 <p class="description">
2901 <?php esc_html_e( 'One IP per line. These IPs will bypass firewall checks.', 'vigilante' ); ?>
2902 <br>
2903 <?php
2904 printf(
2905 /* translators: 1: opening <code>, 2: closing </code>. Placeholders wrap the IP, CIDR and wildcard examples. */
2906 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' ),
2907 '<code>',
2908 '</code>'
2909 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded.
2910 ?>
2911 </p>
2912 </td>
2913 </tr>
2914 <tr>
2915 <th scope="row"><?php esc_html_e( 'IP Blacklist', 'vigilante' ); ?></th>
2916 <td>
2917 <textarea name="firewall[ip_blacklist]" rows="4" class="large-text code" placeholder="203.0.113.42&#10;203.0.113.0/24&#10;203.0.113.*"><?php echo esc_textarea( implode( "\n", $options['ip_blacklist'] ?? array() ) ); ?></textarea>
2918 <p class="description">
2919 <?php esc_html_e( 'One IP per line. These IPs will be blocked immediately.', 'vigilante' ); ?>
2920 <br>
2921 <?php
2922 printf(
2923 /* translators: 1: opening <code>, 2: closing </code>. Placeholders wrap the IP, CIDR and wildcard examples. */
2924 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' ),
2925 '<code>',
2926 '</code>'
2927 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded.
2928 ?>
2929 </p>
2930 </td>
2931 </tr>
2932 </table>
2933
2934 <h3><?php esc_html_e( 'User-Agent Lists', 'vigilante' ); ?></h3>
2935 <p><?php esc_html_e( 'Partial matching: enter a keyword and any User-Agent containing it will be matched.', 'vigilante' ); ?></p>
2936 <table class="form-table">
2937 <tr>
2938 <th scope="row"><?php esc_html_e( 'User-Agent Whitelist', 'vigilante' ); ?></th>
2939 <td>
2940 <textarea name="firewall[ua_whitelist]" rows="4" class="large-text code"><?php echo esc_textarea( implode( "\n", $options['ua_whitelist'] ?? array() ) ); ?></textarea>
2941 <p class="description"><?php esc_html_e( 'One User-Agent per line. These will bypass all firewall checks. Example: ManageWP, MainWP, UptimeRobot.', 'vigilante' ); ?></p>
2942 </td>
2943 </tr>
2944 <tr>
2945 <th scope="row"><?php esc_html_e( 'User-Agent Blacklist', 'vigilante' ); ?></th>
2946 <td>
2947 <textarea name="firewall[ua_blacklist]" rows="4" class="large-text code"><?php echo esc_textarea( implode( "\n", $options['ua_blacklist'] ?? array() ) ); ?></textarea>
2948 <p class="description"><?php esc_html_e( 'One User-Agent per line. These will be blocked immediately.', 'vigilante' ); ?></p>
2949 </td>
2950 </tr>
2951 </table>
2952 </div>
2953
2954 <div id="vigilante-section-firewall-server" class="vigilante-settings-section">
2955 <h2>
2956 <?php esc_html_e( 'Server Protection', 'vigilante' ); ?>
2957 <span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span>
2958 </h2>
2959 <p><?php esc_html_e( 'Server-level rules for Apache/LiteSpeed. These rules are processed before PHP.', 'vigilante' ); ?></p>
2960
2961 <table class="form-table">
2962 <tr id="field-disable-directory-browsing">
2963 <th scope="row"><?php esc_html_e( 'Directory Browsing', 'vigilante' ); ?></th>
2964 <td>
2965 <label>
2966 <input type="checkbox" name="firewall[disable_directory_browsing]" value="1" <?php checked( ! empty( $options['disable_directory_browsing'] ) ); ?>>
2967 <?php esc_html_e( 'Disable directory listing (Options -Indexes)', 'vigilante' ); ?>
2968 </label>
2969 </td>
2970 </tr>
2971 <tr>
2972 <th scope="row"><?php esc_html_e( 'Protect wp-config.php', 'vigilante' ); ?></th>
2973 <td>
2974 <label>
2975 <input type="checkbox" name="firewall[protect_wp_config]" value="1" <?php checked( ! empty( $options['protect_wp_config'] ) ); ?>>
2976 <?php esc_html_e( 'Block direct HTTP access to wp-config.php', 'vigilante' ); ?>
2977 </label>
2978 </td>
2979 </tr>
2980 <tr id="field-protect-wp-cron">
2981 <th scope="row"><?php esc_html_e( 'Protect wp-cron.php', 'vigilante' ); ?></th>
2982 <td>
2983 <label>
2984 <input type="checkbox" name="firewall[protect_wp_cron]" value="1" <?php checked( ! empty( $options['protect_wp_cron'] ) ); ?>>
2985 <?php esc_html_e( 'Block direct HTTP access to wp-cron.php (prevents cron-spam DoS abuse)', 'vigilante' ); ?>
2986 </label>
2987 <p class="description"><?php
2988 printf(
2989 /* translators: 1: opening <strong>, 2: closing </strong> */
2990 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' ),
2991 '<strong>',
2992 '</strong>',
2993 '<code>',
2994 '</code>'
2995 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded.
2996 ?></p>
2997 </td>
2998 </tr>
2999 <tr>
3000 <th scope="row"><?php esc_html_e( 'Protect wp-includes', 'vigilante' ); ?></th>
3001 <td>
3002 <label>
3003 <input type="checkbox" name="firewall[protect_wp_includes]" value="1" <?php checked( ! empty( $options['protect_wp_includes'] ) ); ?>>
3004 <?php esc_html_e( 'Block direct access to PHP files in wp-includes', 'vigilante' ); ?>
3005 </label>
3006 </td>
3007 </tr>
3008 <tr>
3009 <th scope="row"><?php esc_html_e( 'PHP in Uploads', 'vigilante' ); ?></th>
3010 <td>
3011 <label>
3012 <input type="checkbox" name="firewall[protect_uploads_php]" value="1" <?php checked( ! empty( $options['protect_uploads_php'] ) ); ?>>
3013 <?php esc_html_e( 'Block PHP execution in wp-content/uploads', 'vigilante' ); ?>
3014 </label>
3015 </td>
3016 </tr>
3017 <tr>
3018 <th scope="row"><?php esc_html_e( 'Sensitive Files', 'vigilante' ); ?></th>
3019 <td>
3020 <label>
3021 <input type="checkbox" name="firewall[protect_sensitive_files]" value="1" <?php checked( ! empty( $options['protect_sensitive_files'] ) ); ?>>
3022 <?php esc_html_e( 'Block access to .sql, .bak, .log, .ini, readme.html, license.txt, licencia.txt', 'vigilante' ); ?>
3023 </label>
3024 </td>
3025 </tr>
3026 <tr>
3027 <th scope="row"><?php esc_html_e( 'Limit HTTP Methods', 'vigilante' ); ?></th>
3028 <td>
3029 <label>
3030 <input type="checkbox" name="firewall[limit_http_methods]" value="1" <?php checked( ! empty( $options['limit_http_methods'] ) ); ?>>
3031 <?php esc_html_e( 'Allow only GET, POST, HEAD (blocks PUT, DELETE, TRACE, etc.)', 'vigilante' ); ?>
3032 </label>
3033 </td>
3034 </tr>
3035 </table>
3036 </div>
3037
3038 <p class="submit vigilante-submit-buttons">
3039 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
3040 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
3041 </button>
3042 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
3043 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
3044 </button>
3045 </p>
3046 </form>
3047 <?php
3048 }
3049
3050 /**
3051 * Render login security tab
3052 */
3053 private function render_tab_login() {
3054 $is_disabled = $this->render_module_disabled_notice( 'login_security' );
3055 $options = $this->settings->get_section( 'login_security' );
3056 $lockouts = $this->database->get_active_lockouts();
3057 ?>
3058 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="login_security" <?php echo $is_disabled ? 'inert' : ''; ?>>
3059 <div id="vigilante-section-login-main" class="vigilante-settings-section">
3060 <h2>
3061 <?php esc_html_e( 'Login Protection', 'vigilante' ); ?>
3062 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
3063 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
3064 </h2>
3065 <p><?php esc_html_e( 'Brute force protection and WordPress login hardening.', 'vigilante' ); ?></p>
3066
3067 <table class="form-table">
3068 <tr id="field-max-attempts">
3069 <th scope="row"><?php esc_html_e( 'Max Login Attempts', 'vigilante' ); ?></th>
3070 <td>
3071 <input type="number" name="login_security[max_attempts]" value="<?php echo esc_attr( $options['max_attempts'] ?? 5 ); ?>" min="1" max="20" class="small-text">
3072 <p class="description"><?php esc_html_e( 'Number of failed attempts before lockout.', 'vigilante' ); ?></p>
3073 </td>
3074 </tr>
3075 <tr>
3076 <th scope="row"><?php esc_html_e( 'Lockout Duration', 'vigilante' ); ?></th>
3077 <td>
3078 <input type="number" name="login_security[lockout_duration]" value="<?php echo esc_attr( ( $options['lockout_duration'] ?? 1800 ) / 60 ); ?>" min="1" max="1440" class="small-text">
3079 <?php esc_html_e( 'minutes', 'vigilante' ); ?>
3080 </td>
3081 </tr>
3082 <tr>
3083 <th scope="row"><?php esc_html_e( 'Progressive Lockout', 'vigilante' ); ?></th>
3084 <td>
3085 <label>
3086 <input type="checkbox" name="login_security[lockout_increment]" value="1" <?php checked( ! empty( $options['lockout_increment'] ) ); ?>>
3087 <?php esc_html_e( 'Double lockout duration for repeat offenders', 'vigilante' ); ?>
3088 </label>
3089 </td>
3090 </tr>
3091 <tr>
3092 <th scope="row"><?php esc_html_e( 'Hide Login Errors', 'vigilante' ); ?></th>
3093 <td>
3094 <label>
3095 <input type="checkbox" name="login_security[hide_login_errors]" value="1" <?php checked( ! empty( $options['hide_login_errors'] ) ); ?>>
3096 <?php esc_html_e( 'Show generic error message instead of specific errors', 'vigilante' ); ?>
3097 </label>
3098 </td>
3099 </tr>
3100 <tr id="field-disable-xmlrpc">
3101 <th scope="row"><?php esc_html_e( 'Disable XML-RPC', 'vigilante' ); ?></th>
3102 <td>
3103 <label>
3104 <input type="checkbox" name="login_security[disable_xmlrpc]" value="1" <?php checked( ! empty( $options['disable_xmlrpc'] ) ); ?>>
3105 <?php esc_html_e( 'Completely disable XML-RPC functionality', 'vigilante' ); ?>
3106 </label>
3107 </td>
3108 </tr>
3109 <tr>
3110 <th scope="row"><?php esc_html_e( 'Disable XML-RPC Pingback', 'vigilante' ); ?></th>
3111 <td>
3112 <label>
3113 <input type="checkbox" name="login_security[disable_xmlrpc_pingback]" value="1" <?php checked( ! empty( $options['disable_xmlrpc_pingback'] ) ); ?>>
3114 <?php esc_html_e( 'Remove only the pingback methods and keep the rest of XML-RPC working', 'vigilante' ); ?>
3115 </label>
3116 <p class="description"><?php esc_html_e( 'Pingbacks are the part abused for distributed attacks. Use this when something still needs XML-RPC, such as the WordPress mobile app or Jetpack.', 'vigilante' ); ?></p>
3117 </td>
3118 </tr>
3119 <tr>
3120 <th scope="row"><?php esc_html_e( 'Disable Application Passwords', 'vigilante' ); ?></th>
3121 <td>
3122 <label>
3123 <input type="checkbox" name="login_security[disable_application_passwords]" value="1" <?php checked( ! empty( $options['disable_application_passwords'] ) ); ?>>
3124 <?php esc_html_e( 'Disable WordPress application passwords feature', 'vigilante' ); ?>
3125 </label>
3126 </td>
3127 </tr>
3128 </table>
3129
3130 <h3><?php esc_html_e( 'Custom Login URL', 'vigilante' ); ?></h3>
3131 <table class="form-table">
3132 <tr id="field-custom-login-url">
3133 <th scope="row"><?php esc_html_e( 'Login URL Slug', 'vigilante' ); ?></th>
3134 <td>
3135 <code><?php echo esc_url( home_url( '/' ) ); ?></code>
3136 <input type="text" name="login_security[custom_login_url]" id="vigilante_custom_login_url" value="<?php echo esc_attr( $options['custom_login_url'] ?? '' ); ?>" class="regular-text" placeholder="<?php esc_attr_e( 'my-secret-login', 'vigilante' ); ?>">
3137 <p class="description">
3138 <?php esc_html_e( '&#9432; Leave empty to use default wp-login.php. Use only lowercase letters, numbers and hyphens.', 'vigilante' ); ?>
3139 </p>
3140 <div class="vigilante-login-url-preview" <?php echo empty( $options['custom_login_url'] ) ? 'style="display:none;"' : ''; ?>>
3141 <p class="description">
3142 <strong><?php esc_html_e( 'Your login URL:', 'vigilante' ); ?></strong>
3143 <code class="vigilante-login-url-display"><?php echo esc_url( home_url( sanitize_title( $options['custom_login_url'] ?? '' ) . '/' ) ); ?></code>
3144 </p>
3145 <p class="description">
3146 <?php esc_html_e( 'Direct access to wp-login.php and wp-admin will return a 404 error for non-logged users.', 'vigilante' ); ?>
3147 </p>
3148 </div>
3149 </td>
3150 </tr>
3151 </table>
3152
3153 <div class="vigilante-login-url-notify-wrapper <?php echo empty( $options['custom_login_url'] ) ? 'vigilante-login-url-notify-disabled' : ''; ?>">
3154 <table class="form-table">
3155 <tr>
3156 <th scope="row"><?php esc_html_e( 'Notify users', 'vigilante' ); ?></th>
3157 <td>
3158 <label>
3159 <input type="checkbox" name="login_security[notify_on_login_url_change]" id="vigilante_notify_login_url_change" value="1" <?php checked( $options['notify_on_login_url_change'] ?? true ); ?>>
3160 <?php esc_html_e( 'Notify affected users when the login URL changes', 'vigilante' ); ?>
3161 </label>
3162 <div class="vigilante-login-url-send-notification" style="margin-top:12px;">
3163 <button type="button" id="vigilante_notify_login_url" class="button">
3164 <?php esc_html_e( 'Send notification now', 'vigilante' ); ?>
3165 </button>
3166 <span class="vigilante-login-url-notify-status"></span>
3167 <p class="description"><?php esc_html_e( 'Sends the new URL to administrators, editors, authors, and contributors.', 'vigilante' ); ?></p>
3168 </div>
3169 </td>
3170 </tr>
3171 </table>
3172 </div>
3173
3174 <?php $this->render_2fa_settings( $options ); ?>
3175
3176 <h3><?php esc_html_e( 'Notifications', 'vigilante' ); ?></h3>
3177 <table class="form-table">
3178 <tr>
3179 <th scope="row"><?php esc_html_e( 'Notify on Lockout', 'vigilante' ); ?></th>
3180 <td>
3181 <label>
3182 <input type="checkbox" name="login_security[notify_on_lockout]" value="1" <?php checked( ! empty( $options['notify_on_lockout'] ) ); ?>>
3183 <?php esc_html_e( 'Send email when an IP is locked out', 'vigilante' ); ?>
3184 </label>
3185 </td>
3186 </tr>
3187 <tr>
3188 <th scope="row"><?php esc_html_e( 'Notify on Admin Login', 'vigilante' ); ?></th>
3189 <td>
3190 <label>
3191 <input type="checkbox" name="login_security[notify_on_admin_login]" value="1" <?php checked( ! empty( $options['notify_on_admin_login'] ) ); ?>>
3192 <?php esc_html_e( 'Send email when an administrator logs in', 'vigilante' ); ?>
3193 </label>
3194 </td>
3195 </tr>
3196 </table>
3197
3198 <p class="description">
3199 <?php
3200 printf(
3201 /* translators: %s: Link to notification settings */
3202 esc_html__( '&#9432; Notifications are sent to the recipients configured in %s.', 'vigilante' ),
3203 '<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>'
3204 );
3205 ?>
3206 </p>
3207 </div>
3208
3209 <p class="submit vigilante-submit-buttons">
3210 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
3211 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
3212 </button>
3213 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
3214 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
3215 </button>
3216 </p>
3217 </form>
3218
3219 <?php $this->render_lockout_info_section( $options, $lockouts ); ?>
3220 <?php
3221 }
3222
3223 /**
3224 * Render lockout information and blocked IPs section
3225 *
3226 * @param array $options Login security options.
3227 * @param array $lockouts Currently locked out IPs.
3228 */
3229 private function render_lockout_info_section( $options, $lockouts ) {
3230 $max_attempts = absint( $options['max_attempts'] ?? 5 );
3231 $lockout_duration = absint( $options['lockout_duration'] ?? 1800 );
3232 $lockout_increment = ! empty( $options['lockout_increment'] );
3233 $max_lockout = absint( $options['max_lockout_duration'] ?? 86400 );
3234 $two_factor = $options['two_factor'] ?? array();
3235 $two_factor_enabled = ! empty( $two_factor['enabled'] );
3236 ?>
3237 <div class="vigilante-settings-section vigilante-lockout-section">
3238 <h2><?php esc_html_e( 'Login Protection Status', 'vigilante' ); ?></h2>
3239
3240 <table class="form-table">
3241 <tr>
3242 <th scope="row"><?php esc_html_e( 'Current settings', 'vigilante' ); ?></th>
3243 <td>
3244 <?php
3245 printf(
3246 /* translators: 1: Maximum login attempts, 2: Lockout duration in minutes */
3247 esc_html__( 'After %1$d failed login attempts, the IP address is blocked for %2$d minutes.', 'vigilante' ),
3248 absint( $max_attempts ),
3249 absint( ceil( $lockout_duration / 60 ) )
3250 );
3251
3252 if ( $lockout_increment ) {
3253 echo '<br>';
3254 printf(
3255 /* translators: %d: Maximum lockout duration in hours */
3256 esc_html__( 'Progressive lockout enabled (max: %d hours).', 'vigilante' ),
3257 absint( ceil( $max_lockout / 3600 ) )
3258 );
3259 }
3260
3261 if ( $two_factor_enabled ) {
3262 echo '<br>';
3263 esc_html_e( 'Failed 2FA codes also count toward the lockout limit.', 'vigilante' );
3264 }
3265 ?>
3266 </td>
3267 </tr>
3268 <tr>
3269 <th scope="row"><?php esc_html_e( 'Blocked IPs', 'vigilante' ); ?></th>
3270 <td>
3271 <?php if ( ! empty( $lockouts ) ) : ?>
3272 <div class="vigilante-paginated-section">
3273 <div class="vigilante-fi-pagination-wrap"></div>
3274 <table class="wp-list-table widefat fixed striped vigilante-fi-paginated">
3275 <thead>
3276 <tr>
3277 <th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th>
3278 <th><?php esc_html_e( 'Attempts', 'vigilante' ); ?></th>
3279 <th><?php esc_html_e( 'Blocked Until', 'vigilante' ); ?></th>
3280 <th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
3281 </tr>
3282 </thead>
3283 <tbody>
3284 <?php foreach ( $lockouts as $lockout ) :
3285 $lockout_time = strtotime( $lockout->locked_until );
3286 $remaining_seconds = $lockout_time - time();
3287 $remaining_text = $this->format_remaining_time( $remaining_seconds );
3288 ?>
3289 <tr>
3290 <td><code><?php echo esc_html( $lockout->ip_address ); ?></code></td>
3291 <td><?php echo esc_html( $lockout->attempts ); ?></td>
3292 <td>
3293 <?php echo esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $lockout_time ) ); ?>
3294 <br>
3295 <small class="description">
3296 <?php
3297 printf(
3298 /* translators: %s: Remaining time */
3299 esc_html__( '%s remaining', 'vigilante' ),
3300 esc_html( $remaining_text )
3301 );
3302 ?>
3303 </small>
3304 </td>
3305 <td>
3306 <button type="button" class="button button-small vigilante-clear-lockout" data-ip="<?php echo esc_attr( $lockout->ip_address ); ?>">
3307 <?php esc_html_e( 'Unblock', 'vigilante' ); ?>
3308 </button>
3309 </td>
3310 </tr>
3311 <?php endforeach; ?>
3312 </tbody>
3313 </table>
3314 </div>
3315 <p class="description" style="margin-top: 10px;">
3316 <button type="button" class="button vigilante-clear-all-lockouts">
3317 <?php esc_html_e( 'Unblock All IPs', 'vigilante' ); ?>
3318 </button>
3319 <span style="margin-left: 10px;">
3320 <?php
3321 printf(
3322 /* translators: %d: Number of blocked IPs */
3323 esc_html( _n( '%d IP currently blocked', '%d IPs currently blocked', count( $lockouts ), 'vigilante' ) ),
3324 count( $lockouts )
3325 );
3326 ?>
3327 </span>
3328 </p>
3329 <?php else : ?>
3330 <span class="dashicons dashicons-yes-alt" style="color: #00a32a; font-size: 20px; width: 20px; height: 20px; vertical-align: middle;"></span>
3331 <span style="vertical-align: middle; margin-left: 5px;"><?php esc_html_e( 'No IPs are currently blocked. All clear!', 'vigilante' ); ?></span>
3332 <?php endif; ?>
3333 </td>
3334 </tr>
3335 </table>
3336 </div>
3337 <?php
3338 }
3339
3340 /**
3341 * Format remaining lockout time in human readable format
3342 *
3343 * @param int $seconds Remaining seconds.
3344 * @return string Formatted time string.
3345 */
3346 private function format_remaining_time( $seconds ) {
3347 if ( $seconds <= 0 ) {
3348 return __( 'expired', 'vigilante' );
3349 }
3350
3351 if ( $seconds < 60 ) {
3352 return sprintf(
3353 /* translators: %d: Number of seconds */
3354 _n( '%d second', '%d seconds', $seconds, 'vigilante' ),
3355 $seconds
3356 );
3357 }
3358
3359 if ( $seconds < 3600 ) {
3360 $minutes = ceil( $seconds / 60 );
3361 return sprintf(
3362 /* translators: %d: Number of minutes */
3363 _n( '%d minute', '%d minutes', $minutes, 'vigilante' ),
3364 $minutes
3365 );
3366 }
3367
3368 $hours = floor( $seconds / 3600 );
3369 $remaining_minutes = ceil( ( $seconds % 3600 ) / 60 );
3370
3371 if ( $remaining_minutes > 0 ) {
3372 return sprintf(
3373 /* translators: 1: Number of hours, 2: Number of minutes */
3374 __( '%1$d hours %2$d minutes', 'vigilante' ),
3375 $hours,
3376 $remaining_minutes
3377 );
3378 }
3379
3380 return sprintf(
3381 /* translators: %d: Number of hours */
3382 _n( '%d hour', '%d hours', $hours, 'vigilante' ),
3383 $hours
3384 );
3385 }
3386
3387 /**
3388 * Render 2FA settings section
3389 *
3390 * @param array $options Login security options.
3391 */
3392 private function render_2fa_settings( $options ) {
3393 $two_factor = $options['two_factor'] ?? array();
3394 $all_roles = wp_roles()->roles;
3395 $enforced = $two_factor['enforced_roles'] ?? array( 'administrator', 'editor' );
3396 $excluded = $two_factor['excluded_users'] ?? array();
3397 $method = $two_factor['method'] ?? 'email';
3398 $grace_days = $two_factor['grace_period_days'] ?? 3;
3399 ?>
3400 <h3>
3401 <?php esc_html_e( 'Two-Factor Authentication (2FA)', 'vigilante' ); ?>
3402 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
3403 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
3404 </h3>
3405 <p class="description"><?php esc_html_e( 'Require a second verification step after password. Choose between email codes or an authenticator app (TOTP).', 'vigilante' ); ?></p>
3406
3407 <table class="form-table">
3408 <tr>
3409 <th scope="row"><?php esc_html_e( 'Enable 2FA', 'vigilante' ); ?></th>
3410 <td>
3411 <label>
3412 <input type="checkbox" name="login_security[two_factor][enabled]" id="vigilante_2fa_enabled" value="1" <?php checked( ! empty( $two_factor['enabled'] ) ); ?>>
3413 <?php esc_html_e( 'Enable two-factor authentication', 'vigilante' ); ?>
3414 </label>
3415 </td>
3416 </tr>
3417 </table>
3418
3419 <div class="vigilante-2fa-settings-wrapper <?php echo empty( $two_factor['enabled'] ) ? 'vigilante-2fa-settings-disabled' : ''; ?>">
3420 <table class="form-table">
3421 <tr>
3422 <th scope="row"><?php esc_html_e( 'Verification method', 'vigilante' ); ?></th>
3423 <td>
3424 <fieldset class="vigilante-2fa-method-selector">
3425 <label class="vigilante-2fa-method-option <?php echo 'email' === $method ? 'selected' : ''; ?>">
3426 <input type="radio" name="login_security[two_factor][method]" value="email" <?php checked( $method, 'email' ); ?>>
3427 <span class="dashicons dashicons-email"></span>
3428 <span class="method-info">
3429 <strong><?php esc_html_e( 'Email code', 'vigilante' ); ?></strong>
3430 <span><?php esc_html_e( 'Users receive a 6-digit code via email after entering their password.', 'vigilante' ); ?></span>
3431 </span>
3432 </label>
3433 <label class="vigilante-2fa-method-option <?php echo 'totp' === $method ? 'selected' : ''; ?>">
3434 <input type="radio" name="login_security[two_factor][method]" value="totp" <?php checked( $method, 'totp' ); ?>>
3435 <span class="dashicons dashicons-smartphone"></span>
3436 <span class="method-info">
3437 <strong><?php esc_html_e( 'Authenticator app (TOTP)', 'vigilante' ); ?></strong>
3438 <span><?php esc_html_e( 'Users verify with a time-based code from Google Authenticator, Authy, etc.', 'vigilante' ); ?></span>
3439 </span>
3440 </label>
3441 </fieldset>
3442 </td>
3443 </tr>
3444 <tr>
3445 <th scope="row"><?php esc_html_e( 'Enforce for roles', 'vigilante' ); ?></th>
3446 <td>
3447 <div class="vigilante-2fa-roles">
3448 <?php foreach ( $all_roles as $role_slug => $role_data ) :
3449 $user_count = count( get_users( array( 'role' => $role_slug, 'fields' => 'ID' ) ) );
3450 ?>
3451 <label>
3452 <input type="checkbox"
3453 name="login_security[two_factor][enforced_roles][]"
3454 value="<?php echo esc_attr( $role_slug ); ?>"
3455 <?php checked( in_array( $role_slug, $enforced, true ) ); ?>>
3456 <span class="role-name"><?php echo esc_html( translate_user_role( $role_data['name'] ) ); ?></span>
3457 <span class="role-count">(<?php echo esc_html( $user_count ); ?>)</span>
3458 </label>
3459 <?php endforeach; ?>
3460 </div>
3461 <p class="description"><?php esc_html_e( 'Users with these roles will be required to verify with the selected method.', 'vigilante' ); ?></p>
3462 </td>
3463 </tr>
3464 <tr>
3465 <th scope="row"><?php esc_html_e( 'Exclude specific users', 'vigilante' ); ?></th>
3466 <td>
3467 <div class="vigilante-2fa-user-search-container">
3468 <div class="vigilante-2fa-user-search">
3469 <span class="search-icon"></span>
3470 <input type="text"
3471 id="vigilante_2fa_user_search"
3472 placeholder="<?php esc_attr_e( 'Search users by name or email...', 'vigilante' ); ?>"
3473 autocomplete="off">
3474 <div class="vigilante-2fa-search-results"></div>
3475 </div>
3476 <div class="vigilante-2fa-excluded-users">
3477 <?php
3478 foreach ( $excluded as $user_id ) :
3479 $user = get_user_by( 'ID', $user_id );
3480 if ( ! $user ) continue;
3481 ?>
3482 <div class="vigilante-2fa-excluded-user" data-user-id="<?php echo esc_attr( $user_id ); ?>">
3483 <span class="user-display"><?php echo esc_html( $user->display_name . ' (' . $user->user_email . ')' ); ?></span>
3484 <button type="button" class="remove-user" aria-label="<?php esc_attr_e( 'Remove', 'vigilante' ); ?>">&times;</button>
3485 <input type="hidden" name="login_security[two_factor][excluded_users][]" value="<?php echo esc_attr( $user_id ); ?>">
3486 </div>
3487 <?php endforeach; ?>
3488 </div>
3489 </div>
3490 <p class="description"><?php esc_html_e( 'These users will not be required to use 2FA regardless of their role.', 'vigilante' ); ?></p>
3491 </td>
3492 </tr>
3493
3494 <!-- Remember device option -->
3495 <tr>
3496 <th scope="row"><?php esc_html_e( 'Remember device', 'vigilante' ); ?></th>
3497 <td>
3498 <label>
3499 <input type="checkbox" name="login_security[two_factor][allow_remember_device]" value="1" <?php checked( ! empty( $two_factor['allow_remember_device'] ) ); ?>>
3500 <?php esc_html_e( 'Allow users to skip 2FA verification on trusted devices', 'vigilante' ); ?>
3501 </label>
3502 <p class="description">
3503 <?php
3504 printf(
3505 /* translators: %d: Number of days */
3506 esc_html__( 'When enabled, users can check "Remember this device" on the verification screen to skip 2FA for %d days.', 'vigilante' ),
3507 absint( $two_factor['remember_device_days'] ?? 30 )
3508 );
3509 ?>
3510 </p>
3511 </td>
3512 </tr>
3513
3514 <!-- TOTP-specific: Grace period -->
3515 <tr class="vigilante-2fa-totp-only" <?php echo 'totp' !== $method ? 'style="display:none;"' : ''; ?>>
3516 <th scope="row"><?php esc_html_e( 'Grace period', 'vigilante' ); ?></th>
3517 <td>
3518 <input type="number"
3519 name="login_security[two_factor][grace_period_days]"
3520 value="<?php echo esc_attr( $grace_days ); ?>"
3521 min="0" max="30" class="small-text">
3522 <?php esc_html_e( 'days', 'vigilante' ); ?>
3523 <p class="description"><?php esc_html_e( 'Days users have to set up their authenticator app. During this period they can log in without TOTP. Set to 0 for immediate enforcement.', 'vigilante' ); ?></p>
3524 </td>
3525 </tr>
3526
3527 <!-- Email-specific: Sender name -->
3528 <tr class="vigilante-2fa-email-only" <?php echo 'email' !== $method ? 'style="display:none;"' : ''; ?>>
3529 <th scope="row"><?php esc_html_e( 'Email sender name', 'vigilante' ); ?></th>
3530 <td>
3531 <input type="text"
3532 name="login_security[two_factor][email_from_name]"
3533 value="<?php echo esc_attr( $two_factor['email_from_name'] ?? '' ); ?>"
3534 class="regular-text vigilante-2fa-email-from"
3535 placeholder="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?>">
3536 <p class="description"><?php esc_html_e( 'Name shown in verification emails. Leave empty to use site name.', 'vigilante' ); ?></p>
3537 </td>
3538 </tr>
3539
3540 <!-- TOTP-specific: Reset users -->
3541 <tr class="vigilante-2fa-totp-only" <?php echo 'totp' !== $method ? 'style="display:none;"' : ''; ?>>
3542 <th scope="row"><?php esc_html_e( 'Reset user TOTP', 'vigilante' ); ?></th>
3543 <td>
3544 <div class="vigilante-totp-reset-container">
3545 <div class="vigilante-2fa-user-search">
3546 <span class="search-icon"></span>
3547 <input type="text"
3548 id="vigilante_totp_reset_search"
3549 placeholder="<?php esc_attr_e( 'Search users with TOTP configured...', 'vigilante' ); ?>"
3550 autocomplete="off">
3551 <div class="vigilante-totp-reset-results"></div>
3552 </div>
3553 <div class="vigilante-totp-reset-selected"></div>
3554 <button type="button" id="vigilante_totp_reset_btn" class="button" style="display:none;">
3555 <?php esc_html_e( 'Reset selected', 'vigilante' ); ?>
3556 </button>
3557 <span class="vigilante-totp-reset-status"></span>
3558 </div>
3559 <p class="description"><?php esc_html_e( 'Reset TOTP for users who lost access to their authenticator app. They will need to set up again.', 'vigilante' ); ?></p>
3560 </td>
3561 </tr>
3562 </table>
3563
3564 <h3><?php esc_html_e( 'User notification', 'vigilante' ); ?></h3>
3565 <table class="form-table">
3566 <tr>
3567 <th scope="row"><?php esc_html_e( 'Notify on enable', 'vigilante' ); ?></th>
3568 <td>
3569 <label>
3570 <input type="checkbox" name="login_security[two_factor][notify_on_enable]" value="1" <?php checked( $two_factor['notify_on_enable'] ?? true ); ?>>
3571 <?php esc_html_e( 'Send notification email to affected users when 2FA is enabled', 'vigilante' ); ?>
3572 </label>
3573
3574 <div class="vigilante-2fa-notification-options">
3575 <label>
3576 <input type="radio" name="vigilante_2fa_notify_mode" value="all" checked>
3577 <?php esc_html_e( 'Send to all affected users', 'vigilante' ); ?>
3578 </label>
3579 <label>
3580 <input type="radio" name="vigilante_2fa_notify_mode" value="new">
3581 <?php esc_html_e( 'Send only to users not previously notified', 'vigilante' ); ?>
3582 </label>
3583 </div>
3584
3585 <div class="vigilante-2fa-send-notification">
3586 <button type="button" id="vigilante_2fa_send_notification" class="button">
3587 <?php esc_html_e( 'Send notification now', 'vigilante' ); ?>
3588 </button>
3589 <span class="vigilante-2fa-notification-status"></span>
3590 </div>
3591 </td>
3592 </tr>
3593 </table>
3594 </div>
3595 <?php
3596 }
3597
3598 /**
3599 * Render security headers tab
3600 */
3601 private function render_tab_headers() {
3602 $is_disabled = $this->render_module_disabled_notice( 'security_headers' );
3603 $options = $this->settings->get_section( 'security_headers' );
3604 ?>
3605 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="security_headers" <?php echo $is_disabled ? 'inert' : ''; ?>>
3606 <div id="vigilante-section-headers-main" class="vigilante-settings-section">
3607 <h2>
3608 <?php esc_html_e( 'Security Headers', 'vigilante' ); ?>
3609 <span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span>
3610 </h2>
3611 <p><?php esc_html_e( 'HTTP headers sent with every response via .htaccess (mod_headers).', 'vigilante' ); ?></p>
3612
3613 <table class="form-table">
3614 <tr>
3615 <th scope="row"><?php esc_html_e( 'X-Frame-Options', 'vigilante' ); ?></th>
3616 <td>
3617 <select name="security_headers[x_frame_options]">
3618 <option value="" <?php selected( empty( $options['x_frame_options'] ) ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option>
3619 <option value="SAMEORIGIN" <?php selected( $options['x_frame_options'] ?? '', 'SAMEORIGIN' ); ?>>SAMEORIGIN</option>
3620 <option value="DENY" <?php selected( $options['x_frame_options'] ?? '', 'DENY' ); ?>>DENY</option>
3621 </select>
3622 <p class="description"><?php esc_html_e( '&#9432; Prevents clickjacking attacks. Also sets CSP frame-ancestors automatically.', 'vigilante' ); ?></p>
3623 </td>
3624 </tr>
3625 <tr>
3626 <th scope="row"><?php esc_html_e( 'X-Content-Type-Options', 'vigilante' ); ?></th>
3627 <td>
3628 <label>
3629 <input type="checkbox" name="security_headers[x_content_type_options]" value="1" <?php checked( ! empty( $options['x_content_type_options'] ) ); ?>>
3630 <?php esc_html_e( 'Add nosniff header to prevent MIME type sniffing', 'vigilante' ); ?>
3631 </label>
3632 </td>
3633 </tr>
3634 <tr>
3635 <th scope="row"><?php esc_html_e( 'Referrer-Policy', 'vigilante' ); ?></th>
3636 <td>
3637 <select name="security_headers[referrer_policy]">
3638 <option value="" <?php selected( empty( $options['referrer_policy'] ) ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option>
3639 <option value="no-referrer" <?php selected( $options['referrer_policy'] ?? '', 'no-referrer' ); ?>>no-referrer</option>
3640 <option value="strict-origin-when-cross-origin" <?php selected( $options['referrer_policy'] ?? '', 'strict-origin-when-cross-origin' ); ?>>strict-origin-when-cross-origin</option>
3641 <option value="same-origin" <?php selected( $options['referrer_policy'] ?? '', 'same-origin' ); ?>>same-origin</option>
3642 </select>
3643 </td>
3644 </tr>
3645 </table>
3646
3647 <h3><?php esc_html_e( 'HSTS (HTTP Strict Transport Security)', 'vigilante' ); ?></h3>
3648 <table class="form-table">
3649 <tr>
3650 <th scope="row"><?php esc_html_e( 'Enable HSTS', 'vigilante' ); ?></th>
3651 <td>
3652 <label>
3653 <input type="checkbox" name="security_headers[hsts][enabled]" value="1" <?php checked( ! empty( $options['hsts']['enabled'] ) ); ?>>
3654 <?php esc_html_e( 'Force HTTPS connections', 'vigilante' ); ?>
3655 </label>
3656 <p class="description"><?php esc_html_e( '&#9888; Warning: Only enable if your site fully supports HTTPS.', 'vigilante' ); ?></p>
3657 </td>
3658 </tr>
3659 <tr>
3660 <th scope="row"><?php esc_html_e( 'Max Age', 'vigilante' ); ?></th>
3661 <td>
3662 <select name="security_headers[hsts][max_age]">
3663 <option value="86400" <?php selected( $options['hsts']['max_age'] ?? 31536000, 86400 ); ?>><?php esc_html_e( '1 day (testing)', 'vigilante' ); ?></option>
3664 <option value="2592000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 2592000 ); ?>><?php esc_html_e( '30 days', 'vigilante' ); ?></option>
3665 <option value="31536000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 31536000 ); ?>><?php esc_html_e( '1 year (recommended)', 'vigilante' ); ?></option>
3666 <option value="63072000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 63072000 ); ?>><?php esc_html_e( '2 years', 'vigilante' ); ?></option>
3667 </select>
3668 </td>
3669 </tr>
3670 <tr>
3671 <th scope="row"><?php esc_html_e( 'Include Subdomains', 'vigilante' ); ?></th>
3672 <td>
3673 <label>
3674 <input type="checkbox" name="security_headers[hsts][include_subdomains]" value="1" <?php checked( ! empty( $options['hsts']['include_subdomains'] ) ); ?>>
3675 <?php esc_html_e( 'Apply HSTS to all subdomains', 'vigilante' ); ?>
3676 </label>
3677 </td>
3678 </tr>
3679 </table>
3680
3681 <h3><?php esc_html_e( 'Content Security Policy', 'vigilante' ); ?></h3>
3682 <table class="form-table">
3683 <tr>
3684 <th scope="row"><?php esc_html_e( 'Enable CSP', 'vigilante' ); ?></th>
3685 <td>
3686 <label>
3687 <input type="checkbox" name="security_headers[csp][enabled]" value="1" <?php checked( ! empty( $options['csp']['enabled'] ) ); ?>>
3688 <?php esc_html_e( 'Enable Content Security Policy', 'vigilante' ); ?>
3689 </label>
3690 </td>
3691 </tr>
3692 <tr>
3693 <th scope="row"><?php esc_html_e( 'Report Only Mode', 'vigilante' ); ?></th>
3694 <td>
3695 <label>
3696 <input type="checkbox" name="security_headers[csp][report_only]" value="1" <?php checked( ! empty( $options['csp']['report_only'] ) ); ?>>
3697 <?php esc_html_e( 'Report violations without blocking (for testing)', 'vigilante' ); ?>
3698 </label>
3699 </td>
3700 </tr>
3701 </table>
3702
3703 <h3><?php esc_html_e( 'HTTPS', 'vigilante' ); ?></h3>
3704 <p class="description"><?php esc_html_e( 'HTTPS is strongly recommended, but Vigilant will not impose it. Enable only what your site already supports.', 'vigilante' ); ?></p>
3705 <table class="form-table">
3706 <tr>
3707 <th scope="row"><?php esc_html_e( 'Redirect HTTP to HTTPS', 'vigilante' ); ?></th>
3708 <td>
3709 <label>
3710 <input type="checkbox" name="security_headers[redirect_http_to_https]" value="1" <?php checked( ! empty( $options['redirect_http_to_https'] ) ); ?>>
3711 <?php esc_html_e( 'Send visitors arriving over HTTP to the HTTPS address', 'vigilante' ); ?>
3712 </label>
3713 <p class="description"><?php esc_html_e( 'Only applies when the site address is already an https:// one. On a site still published over HTTP it does nothing, so it cannot leave the site unreachable.', 'vigilante' ); ?></p>
3714 </td>
3715 </tr>
3716 <tr>
3717 <th scope="row"><?php esc_html_e( 'Fix Mixed Content', 'vigilante' ); ?></th>
3718 <td>
3719 <label>
3720 <input type="checkbox" name="security_headers[fix_mixed_content]" value="1" <?php checked( ! empty( $options['fix_mixed_content'] ) ); ?>>
3721 <?php esc_html_e( 'Rewrite http:// resources to https:// and ask browsers to upgrade the rest', 'vigilante' ); ?>
3722 </label>
3723 </td>
3724 </tr>
3725 <tr>
3726 <th scope="row"><?php esc_html_e( 'Rewrite Site Address on Activation', 'vigilante' ); ?></th>
3727 <td>
3728 <label>
3729 <input type="checkbox" name="security_headers[force_https]" value="1" <?php checked( ! empty( $options['force_https'] ) ); ?>>
3730 <?php esc_html_e( 'Change the WordPress and site addresses to https:// when the plugin is activated', 'vigilante' ); ?>
3731 </label>
3732 <p class="description"><?php esc_html_e( '&#9888; Off by default. This writes to the WordPress Address and Site Address settings, and turning the plugin off later does not undo it. It only runs on activation, and only when the site answers over HTTPS.', 'vigilante' ); ?></p>
3733 </td>
3734 </tr>
3735 </table>
3736
3737 <h3><?php esc_html_e( 'Server Identity', 'vigilante' ); ?></h3>
3738 <p class="description"><?php esc_html_e( 'Hide identifying information that servers expose in responses.', 'vigilante' ); ?></p>
3739 <table class="form-table">
3740 <tr>
3741 <th scope="row"><?php esc_html_e( 'Server Signature', 'vigilante' ); ?></th>
3742 <td>
3743 <label>
3744 <input type="checkbox" name="security_headers[hide_server_signature]" value="1" <?php checked( ! empty( $options['hide_server_signature'] ) ); ?>>
3745 <?php esc_html_e( 'Hide server signature (ServerSignature Off)', 'vigilante' ); ?>
3746 </label>
3747 </td>
3748 </tr>
3749 <tr>
3750 <th scope="row"><?php esc_html_e( 'Remove Fingerprinting Headers', 'vigilante' ); ?></th>
3751 <td>
3752 <label>
3753 <input type="checkbox" name="security_headers[remove_fingerprinting_headers]" value="1" <?php checked( ! empty( $options['remove_fingerprinting_headers'] ) ); ?>>
3754 <?php esc_html_e( 'Remove X-Powered-By and Server headers', 'vigilante' ); ?>
3755 </label>
3756 </td>
3757 </tr>
3758 </table>
3759 </div>
3760
3761 <p class="submit vigilante-submit-buttons">
3762 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
3763 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
3764 </button>
3765 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
3766 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
3767 </button>
3768 <button type="button" class="button vigilante-test-headers">
3769 <?php esc_html_e( 'Test Headers', 'vigilante' ); ?>
3770 </button>
3771 </p>
3772 </form>
3773
3774 <div id="vigilante-headers-result"></div>
3775 <?php
3776 }
3777
3778 /**
3779 * Render REST API tab
3780 */
3781 private function render_tab_rest_api() {
3782 $is_disabled = $this->render_module_disabled_notice( 'rest_api_security' );
3783 $options = $this->settings->get_section( 'rest_api_security' );
3784 ?>
3785 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="rest_api_security" <?php echo $is_disabled ? 'inert' : ''; ?>>
3786 <div id="vigilante-section-rest-api-main" class="vigilante-settings-section">
3787 <h2>
3788 <?php esc_html_e( 'REST API Security', 'vigilante' ); ?>
3789 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
3790 </h2>
3791 <p><?php esc_html_e( 'Control access to WordPress REST API endpoints.', 'vigilante' ); ?></p>
3792
3793 <table class="form-table">
3794 <tr>
3795 <th scope="row"><?php esc_html_e( 'Access Mode', 'vigilante' ); ?></th>
3796 <td>
3797 <select name="rest_api_security[mode]">
3798 <option value="open" <?php selected( $options['mode'] ?? 'selective', 'open' ); ?>><?php esc_html_e( 'Open - Allow all requests', 'vigilante' ); ?></option>
3799 <option value="selective" <?php selected( $options['mode'] ?? 'selective', 'selective' ); ?>><?php esc_html_e( 'Selective - Protect sensitive endpoints', 'vigilante' ); ?></option>
3800 <option value="authenticated_only" <?php selected( $options['mode'] ?? 'selective', 'authenticated_only' ); ?>><?php esc_html_e( 'Authenticated - Require login for all', 'vigilante' ); ?></option>
3801 </select>
3802 <p class="description"><?php esc_html_e( 'Selective mode is recommended.', 'vigilante' ); ?></p>
3803 </td>
3804 </tr>
3805 <tr id="field-block-user-enumeration">
3806 <th scope="row"><?php esc_html_e( 'Block User Enumeration', 'vigilante' ); ?></th>
3807 <td>
3808 <label>
3809 <input type="checkbox" name="rest_api_security[block_user_enumeration]" value="1" <?php checked( ! empty( $options['block_user_enumeration'] ) ); ?>>
3810 <?php esc_html_e( 'Protect /wp/v2/users endpoint for unauthenticated users', 'vigilante' ); ?>
3811 </label>
3812 </td>
3813 </tr>
3814 <tr>
3815 <th scope="row"><?php esc_html_e( 'Disable JSONP', 'vigilante' ); ?></th>
3816 <td>
3817 <label>
3818 <input type="checkbox" name="rest_api_security[disable_jsonp]" value="1" <?php checked( ! empty( $options['disable_jsonp'] ) ); ?>>
3819 <?php esc_html_e( 'Disable JSONP support in REST API', 'vigilante' ); ?>
3820 </label>
3821 </td>
3822 </tr>
3823 </table>
3824 </div>
3825
3826 <p class="submit vigilante-submit-buttons">
3827 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
3828 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
3829 </button>
3830 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
3831 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
3832 </button>
3833 </p>
3834 </form>
3835 <?php
3836 }
3837
3838 /**
3839 * Render User Security tab
3840 */
3841 private function render_tab_users() {
3842 $is_disabled = $this->render_module_disabled_notice( 'user_security' );
3843 $options = $this->settings->get_section( 'user_security' );
3844 $monitoring = $options['admin_monitoring'] ?? array();
3845 $registration = $options['registration_approval'] ?? array();
3846 $session_limits = $options['session_limits'] ?? array();
3847 $password_exp = $options['password_expiration'] ?? array();
3848 $email_verify = $options['email_verification'] ?? array();
3849 ?>
3850
3851 <!-- ============================================================
3852 SETTINGS SECTION - Single form for all configuration
3853 ============================================================ -->
3854 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="user_security" <?php echo $is_disabled ? 'inert' : ''; ?>>
3855
3856 <!-- Username & Password Protection -->
3857 <div id="vigilante-section-users-password" class="vigilante-settings-section">
3858 <h2>
3859 <?php esc_html_e( 'Username & password protection', 'vigilante' ); ?>
3860 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
3861 </h2>
3862 <p><?php esc_html_e( 'Enforce secure username and password policies.', 'vigilante' ); ?></p>
3863
3864 <table class="form-table">
3865 <tr>
3866 <th scope="row"><?php esc_html_e( 'Block Insecure Usernames', 'vigilante' ); ?></th>
3867 <td>
3868 <label>
3869 <input type="checkbox" name="user_security[block_insecure_usernames]" value="1" <?php checked( ! empty( $options['block_insecure_usernames'] ) ); ?>>
3870 <?php esc_html_e( 'Prevent creation of users with common usernames (admin, administrator, etc.)', 'vigilante' ); ?>
3871 </label>
3872 </td>
3873 </tr>
3874 <?php
3875 $pw_policy = wp_parse_args(
3876 ( isset( $options['password_policy'] ) && is_array( $options['password_policy'] ) ) ? $options['password_policy'] : array(),
3877 array(
3878 'require_uppercase' => true,
3879 'require_lowercase' => true,
3880 'require_number' => true,
3881 'require_special' => true,
3882 'block_common' => true,
3883 'block_username' => false,
3884 'affected_roles' => array(),
3885 )
3886 );
3887 $pw_policy_roles = (array) $pw_policy['affected_roles'];
3888 ?>
3889 <tr>
3890 <th scope="row"><?php esc_html_e( 'Enforce Strong Passwords', 'vigilante' ); ?></th>
3891 <td>
3892 <label>
3893 <input type="checkbox" name="user_security[force_strong_passwords]" value="1" <?php checked( ! empty( $options['force_strong_passwords'] ) ); ?>>
3894 <?php esc_html_e( 'Check passwords against the requirements below when a user sets or changes one', 'vigilante' ); ?>
3895 </label>
3896 </td>
3897 </tr>
3898 <tr>
3899 <th scope="row"><?php esc_html_e( 'Minimum Password Length', 'vigilante' ); ?></th>
3900 <td>
3901 <input type="number" name="user_security[min_password_length]" value="<?php echo esc_attr( $options['min_password_length'] ?? 12 ); ?>" min="6" max="32" class="small-text">
3902 <?php esc_html_e( 'characters', 'vigilante' ); ?>
3903 </td>
3904 </tr>
3905 <tr>
3906 <th scope="row"><?php esc_html_e( 'Password Requirements', 'vigilante' ); ?></th>
3907 <td>
3908 <label style="display:block;margin-bottom:5px;">
3909 <input type="checkbox" name="user_security[password_policy][require_uppercase]" value="1" <?php checked( ! empty( $pw_policy['require_uppercase'] ) ); ?>>
3910 <?php esc_html_e( 'Require an uppercase letter (A-Z)', 'vigilante' ); ?>
3911 </label>
3912 <label style="display:block;margin-bottom:5px;">
3913 <input type="checkbox" name="user_security[password_policy][require_lowercase]" value="1" <?php checked( ! empty( $pw_policy['require_lowercase'] ) ); ?>>
3914 <?php esc_html_e( 'Require a lowercase letter (a-z)', 'vigilante' ); ?>
3915 </label>
3916 <label style="display:block;margin-bottom:5px;">
3917 <input type="checkbox" name="user_security[password_policy][require_number]" value="1" <?php checked( ! empty( $pw_policy['require_number'] ) ); ?>>
3918 <?php esc_html_e( 'Require a number (0-9)', 'vigilante' ); ?>
3919 </label>
3920 <label style="display:block;margin-bottom:5px;">
3921 <input type="checkbox" name="user_security[password_policy][require_special]" value="1" <?php checked( ! empty( $pw_policy['require_special'] ) ); ?>>
3922 <?php esc_html_e( 'Require a special character (!, @, #, ...)', 'vigilante' ); ?>
3923 </label>
3924 <label style="display:block;margin-bottom:5px;">
3925 <input type="checkbox" name="user_security[password_policy][block_common]" value="1" <?php checked( ! empty( $pw_policy['block_common'] ) ); ?>>
3926 <?php esc_html_e( 'Reject well-known common passwords', 'vigilante' ); ?>
3927 </label>
3928 <label style="display:block;margin-bottom:5px;">
3929 <input type="checkbox" name="user_security[password_policy][block_username]" value="1" <?php checked( ! empty( $pw_policy['block_username'] ) ); ?>>
3930 <?php esc_html_e( 'Do not allow the username inside the password', 'vigilante' ); ?>
3931 </label>
3932 <p class="description"><?php esc_html_e( 'These rules apply only while "Enforce Strong Passwords" is on. Turn off individual rules to allow, for example, long passphrases without numbers or symbols.', 'vigilante' ); ?></p>
3933 </td>
3934 </tr>
3935 <tr>
3936 <th scope="row"><?php esc_html_e( 'Apply Password Rules To', 'vigilante' ); ?></th>
3937 <td>
3938 <?php foreach ( wp_roles()->get_names() as $role_slug => $role_name ) : ?>
3939 <label style="display:block;margin-bottom:5px;">
3940 <input type="checkbox" name="user_security[password_policy][affected_roles][]" value="<?php echo esc_attr( $role_slug ); ?>" <?php checked( empty( $pw_policy_roles ) || in_array( $role_slug, $pw_policy_roles, true ) ); ?>>
3941 <?php echo esc_html( translate_user_role( $role_name ) ); ?>
3942 </label>
3943 <?php endforeach; ?>
3944 <p class="description"><?php esc_html_e( 'All roles are covered by default. Uncheck a role to exclude it from the password policy.', 'vigilante' ); ?></p>
3945 </td>
3946 </tr>
3947 <tr id="field-block-author-scanning">
3948 <th scope="row"><?php esc_html_e( 'Block Author Scanning', 'vigilante' ); ?></th>
3949 <td>
3950 <label>
3951 <input type="checkbox" name="user_security[block_author_scanning]" value="1" <?php checked( ! empty( $options['block_author_scanning'] ) ); ?>>
3952 <?php esc_html_e( 'Prevent username discovery via ?author=N URLs', 'vigilante' ); ?>
3953 </label>
3954 </td>
3955 </tr>
3956 <tr>
3957 <th scope="row"><?php esc_html_e( 'Display Name Protection', 'vigilante' ); ?></th>
3958 <td>
3959 <label>
3960 <input type="checkbox" name="user_security[prevent_display_name_login_match]" value="1" <?php checked( ! empty( $options['prevent_display_name_login_match'] ) ); ?>>
3961 <?php esc_html_e( 'Prevent users from saving a display name that matches their login username', 'vigilante' ); ?>
3962 </label>
3963 <p class="description"><?php esc_html_e( 'The display name is publicly visible and should not reveal the login username.', 'vigilante' ); ?></p>
3964 </td>
3965 </tr>
3966 </table>
3967 </div>
3968
3969 <!-- Admin Monitoring -->
3970 <div id="vigilante-section-users-admin-monitoring" class="vigilante-settings-section">
3971 <h2>
3972 <?php esc_html_e( 'Admin monitoring', 'vigilante' ); ?>
3973 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
3974 </h2>
3975 <p><?php esc_html_e( 'Receive email alerts when administrator accounts are modified. All events are always logged to the Security Audit.', 'vigilante' ); ?></p>
3976
3977 <table class="form-table">
3978 <tr>
3979 <th scope="row"><?php esc_html_e( 'New Administrator Alert', 'vigilante' ); ?></th>
3980 <td>
3981 <label>
3982 <input type="checkbox" name="user_security[admin_monitoring][alert_new_admin]" value="1" <?php checked( ! empty( $monitoring['alert_new_admin'] ) ); ?>>
3983 <?php esc_html_e( 'Send email alert when a new administrator account is created', 'vigilante' ); ?>
3984 </label>
3985 </td>
3986 </tr>
3987 <tr>
3988 <th scope="row"><?php esc_html_e( 'Admin Email Change Alert', 'vigilante' ); ?></th>
3989 <td>
3990 <label>
3991 <input type="checkbox" name="user_security[admin_monitoring][alert_admin_email_change]" value="1" <?php checked( ! empty( $monitoring['alert_admin_email_change'] ) ); ?>>
3992 <?php esc_html_e( 'Send email alert when an administrator email address is changed', 'vigilante' ); ?>
3993 </label>
3994 </td>
3995 </tr>
3996 <tr>
3997 <th scope="row"><?php esc_html_e( 'Permission Elevation Alert', 'vigilante' ); ?></th>
3998 <td>
3999 <label>
4000 <input type="checkbox" name="user_security[admin_monitoring][alert_permission_elevation]" value="1" <?php checked( ! empty( $monitoring['alert_permission_elevation'] ) ); ?>>
4001 <?php esc_html_e( 'Send email alert when a user is elevated to administrator role', 'vigilante' ); ?>
4002 </label>
4003 </td>
4004 </tr>
4005 <tr>
4006 <th scope="row"><?php esc_html_e( 'Admin Password Change Alert', 'vigilante' ); ?></th>
4007 <td>
4008 <label>
4009 <input type="checkbox" name="user_security[admin_monitoring][alert_admin_password_change]" value="1" <?php checked( ! empty( $monitoring['alert_admin_password_change'] ) ); ?>>
4010 <?php esc_html_e( 'Send email alert when an administrator password is changed', 'vigilante' ); ?>
4011 </label>
4012 </td>
4013 </tr>
4014 </table>
4015
4016 <p class="description">
4017 <?php
4018 printf(
4019 /* translators: %s: Link to notification settings */
4020 esc_html__( '&#9432; Notifications are sent to the recipients configured in %s.', 'vigilante' ),
4021 '<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>'
4022 );
4023 ?>
4024 </p>
4025 </div>
4026
4027 <!-- Registration Approval -->
4028 <div id="vigilante-section-users-registration" class="vigilante-settings-section">
4029 <h2>
4030 <?php esc_html_e( 'Registration approval', 'vigilante' ); ?>
4031 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4032 </h2>
4033 <p><?php esc_html_e( 'Require manual approval for new user registrations.', 'vigilante' ); ?></p>
4034
4035 <table class="form-table">
4036 <tr>
4037 <th scope="row"><?php esc_html_e( 'Enable Registration Approval', 'vigilante' ); ?></th>
4038 <td>
4039 <label>
4040 <input type="checkbox" name="user_security[registration_approval][enabled]" value="1" <?php checked( ! empty( $registration['enabled'] ) ); ?>>
4041 <?php esc_html_e( 'New users must be approved by an administrator before they can log in', 'vigilante' ); ?>
4042 </label>
4043 </td>
4044 </tr>
4045 <tr>
4046 <th scope="row"><?php esc_html_e( 'Notify Admin', 'vigilante' ); ?></th>
4047 <td>
4048 <label>
4049 <input type="checkbox" name="user_security[registration_approval][notify_admin]" value="1" <?php checked( ! empty( $registration['notify_admin'] ) ); ?>>
4050 <?php esc_html_e( 'Send email notification when a new user registers', 'vigilante' ); ?>
4051 </label>
4052 <p class="description"><?php esc_html_e( 'Disable on high-traffic sites to avoid email overload.', 'vigilante' ); ?></p>
4053 </td>
4054 </tr>
4055 <tr>
4056 <th scope="row"><?php esc_html_e( 'Auto-reject After', 'vigilante' ); ?></th>
4057 <td>
4058 <input type="number" name="user_security[registration_approval][auto_reject_days]" value="<?php echo esc_attr( $registration['auto_reject_days'] ?? 0 ); ?>" min="0" max="365" class="small-text">
4059 <?php esc_html_e( 'days (0 = never)', 'vigilante' ); ?>
4060 <p class="description"><?php esc_html_e( 'Automatically reject pending registrations after this many days.', 'vigilante' ); ?></p>
4061 </td>
4062 </tr>
4063 </table>
4064 </div>
4065
4066 <!-- Session Limits -->
4067 <div id="vigilante-section-users-sessions" class="vigilante-settings-section">
4068 <h2>
4069 <?php esc_html_e( 'Session limits', 'vigilante' ); ?>
4070 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4071 </h2>
4072 <p><?php esc_html_e( 'Limit the number of simultaneous sessions per user.', 'vigilante' ); ?></p>
4073
4074 <table class="form-table">
4075 <tr>
4076 <th scope="row"><?php esc_html_e( 'Enable Session Limits', 'vigilante' ); ?></th>
4077 <td>
4078 <label>
4079 <input type="checkbox" name="user_security[session_limits][enabled]" value="1" <?php checked( ! empty( $session_limits['enabled'] ) ); ?>>
4080 <?php esc_html_e( 'Limit the number of active sessions per user', 'vigilante' ); ?>
4081 </label>
4082 </td>
4083 </tr>
4084 <tr>
4085 <th scope="row"><?php esc_html_e( 'Maximum Sessions', 'vigilante' ); ?></th>
4086 <td>
4087 <input type="number" name="user_security[session_limits][max_sessions]" value="<?php echo esc_attr( $session_limits['max_sessions'] ?? 3 ); ?>" min="1" max="10" class="small-text">
4088 <?php esc_html_e( 'sessions per user', 'vigilante' ); ?>
4089 </td>
4090 </tr>
4091 <tr>
4092 <th scope="row"><?php esc_html_e( 'When Limit Exceeded', 'vigilante' ); ?></th>
4093 <td>
4094 <select name="user_security[session_limits][behavior]">
4095 <option value="block_new" <?php selected( ( $session_limits['behavior'] ?? 'close_oldest' ), 'block_new' ); ?>><?php esc_html_e( 'Block new login', 'vigilante' ); ?></option>
4096 <option value="close_oldest" <?php selected( ( $session_limits['behavior'] ?? 'close_oldest' ), 'close_oldest' ); ?>><?php esc_html_e( 'Close oldest session', 'vigilante' ); ?></option>
4097 </select>
4098 <p class="description"><?php esc_html_e( '"Close oldest" is recommended for security - ensures attackers cannot lock out legitimate users.', 'vigilante' ); ?></p>
4099 </td>
4100 </tr>
4101 <tr>
4102 <th scope="row"><?php esc_html_e( 'Exclude Administrators', 'vigilante' ); ?></th>
4103 <td>
4104 <label>
4105 <input type="checkbox" name="user_security[session_limits][exclude_admins]" value="1" <?php checked( ! empty( $session_limits['exclude_admins'] ) ); ?>>
4106 <?php esc_html_e( 'Do not apply session limits to administrators', 'vigilante' ); ?>
4107 </label>
4108 </td>
4109 </tr>
4110 </table>
4111 </div>
4112
4113 <!-- Password Expiration -->
4114 <div id="vigilante-section-users-password-exp" class="vigilante-settings-section">
4115 <h2>
4116 <?php esc_html_e( 'Password expiration', 'vigilante' ); ?>
4117 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4118 </h2>
4119 <p><?php esc_html_e( 'Force users to change their password periodically.', 'vigilante' ); ?></p>
4120
4121 <table class="form-table">
4122 <tr>
4123 <th scope="row"><?php esc_html_e( 'Enable Password Expiration', 'vigilante' ); ?></th>
4124 <td>
4125 <label>
4126 <input type="checkbox" name="user_security[password_expiration][enabled]" value="1" <?php checked( ! empty( $password_exp['enabled'] ) ); ?>>
4127 <?php esc_html_e( 'Force password change after a set number of days', 'vigilante' ); ?>
4128 </label>
4129 </td>
4130 </tr>
4131 <tr>
4132 <th scope="row"><?php esc_html_e( 'Expire After', 'vigilante' ); ?></th>
4133 <td>
4134 <input type="number" name="user_security[password_expiration][expire_days]" value="<?php echo esc_attr( $password_exp['expire_days'] ?? 90 ); ?>" min="7" max="365" class="small-text">
4135 <?php esc_html_e( 'days', 'vigilante' ); ?>
4136 <p class="description"><?php esc_html_e( 'PCI-DSS recommends 90 days.', 'vigilante' ); ?></p>
4137 </td>
4138 </tr>
4139 <tr>
4140 <th scope="row"><?php esc_html_e( 'Warning Period', 'vigilante' ); ?></th>
4141 <td>
4142 <input type="number" name="user_security[password_expiration][warning_days]" value="<?php echo esc_attr( $password_exp['warning_days'] ?? 14 ); ?>" min="1" max="30" class="small-text">
4143 <?php esc_html_e( 'days before expiration', 'vigilante' ); ?>
4144 <p class="description"><?php esc_html_e( 'Show warning notice this many days before password expires.', 'vigilante' ); ?></p>
4145 </td>
4146 </tr>
4147 <tr>
4148 <th scope="row"><?php esc_html_e( 'Password History', 'vigilante' ); ?></th>
4149 <td>
4150 <input type="number" name="user_security[password_expiration][password_history]" value="<?php echo esc_attr( $password_exp['password_history'] ?? 3 ); ?>" min="0" max="24" class="small-text">
4151 <?php esc_html_e( 'passwords to remember', 'vigilante' ); ?>
4152 <p class="description"><?php esc_html_e( 'Prevent reusing recent passwords. Set to 0 to disable.', 'vigilante' ); ?></p>
4153 </td>
4154 </tr>
4155 <tr>
4156 <th scope="row"><?php esc_html_e( 'Email Reminder', 'vigilante' ); ?></th>
4157 <td>
4158 <label>
4159 <input type="checkbox" name="user_security[password_expiration][send_reminder]" value="1" <?php checked( ! empty( $password_exp['send_reminder'] ) ); ?>>
4160 <?php esc_html_e( 'Send email reminder when password is about to expire', 'vigilante' ); ?>
4161 </label>
4162 <p class="description"><?php esc_html_e( 'The reminder is sent once when the warning period starts, using the same number of days configured above.', 'vigilante' ); ?></p>
4163 </td>
4164 </tr>
4165 <tr>
4166 <th scope="row"><?php esc_html_e( 'Affected Roles', 'vigilante' ); ?></th>
4167 <td>
4168 <?php
4169 $affected_roles = $password_exp['affected_roles'] ?? array( 'administrator', 'editor' );
4170 $all_roles = wp_roles()->get_names();
4171 foreach ( $all_roles as $role_slug => $role_name ) :
4172 ?>
4173 <label style="display: block; margin-bottom: 5px;">
4174 <input type="checkbox" name="user_security[password_expiration][affected_roles][]" value="<?php echo esc_attr( $role_slug ); ?>" <?php checked( in_array( $role_slug, $affected_roles, true ) ); ?>>
4175 <?php echo esc_html( translate_user_role( $role_name ) ); ?>
4176 </label>
4177 <?php endforeach; ?>
4178 </td>
4179 </tr>
4180 <tr>
4181 <th scope="row"><?php esc_html_e( 'Exclude specific users', 'vigilante' ); ?></th>
4182 <td>
4183 <?php $pwexp_excluded = $password_exp['excluded_users'] ?? array(); ?>
4184 <div class="vigilante-2fa-user-search-container">
4185 <div class="vigilante-2fa-user-search">
4186 <span class="search-icon"></span>
4187 <input type="text"
4188 id="vigilante_pwexp_user_search"
4189 placeholder="<?php esc_attr_e( 'Search users by name or email...', 'vigilante' ); ?>"
4190 autocomplete="off">
4191 <div class="vigilante-pwexp-search-results"></div>
4192 </div>
4193 <div class="vigilante-pwexp-excluded-users">
4194 <?php
4195 foreach ( $pwexp_excluded as $pwexp_excluded_id ) :
4196 $excluded_user = get_user_by( 'ID', $pwexp_excluded_id );
4197 if ( ! $excluded_user ) {
4198 continue;
4199 }
4200 ?>
4201 <div class="vigilante-pwexp-excluded-user" data-user-id="<?php echo esc_attr( $pwexp_excluded_id ); ?>">
4202 <span class="user-display"><?php echo esc_html( $excluded_user->display_name . ' (' . $excluded_user->user_email . ')' ); ?></span>
4203 <button type="button" class="remove-user" aria-label="<?php esc_attr_e( 'Remove', 'vigilante' ); ?>">&times;</button>
4204 <input type="hidden" name="user_security[password_expiration][excluded_users][]" value="<?php echo esc_attr( $pwexp_excluded_id ); ?>">
4205 </div>
4206 <?php endforeach; ?>
4207 </div>
4208 </div>
4209 <p class="description"><?php esc_html_e( 'Listed users will be excluded from password expiration regardless of their role.', 'vigilante' ); ?></p>
4210 </td>
4211 </tr>
4212 </table>
4213 </div>
4214
4215 <!-- Email Verification -->
4216 <div id="vigilante-section-users-email-verify" class="vigilante-settings-section">
4217 <h2>
4218 <?php esc_html_e( 'Email verification', 'vigilante' ); ?>
4219 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4220 </h2>
4221 <p><?php esc_html_e( 'Require new users to verify their email address before logging in.', 'vigilante' ); ?></p>
4222
4223 <table class="form-table">
4224 <tr>
4225 <th scope="row"><?php esc_html_e( 'Enable Email Verification', 'vigilante' ); ?></th>
4226 <td>
4227 <label>
4228 <input type="checkbox" name="user_security[email_verification][enabled]" value="1" <?php checked( ! empty( $email_verify['enabled'] ) ); ?>>
4229 <?php esc_html_e( 'New users must verify their email before logging in', 'vigilante' ); ?>
4230 </label>
4231 </td>
4232 </tr>
4233 <tr>
4234 <th scope="row"><?php esc_html_e( 'Link Expiration', 'vigilante' ); ?></th>
4235 <td>
4236 <input type="number" name="user_security[email_verification][token_expiry_hours]" value="<?php echo esc_attr( $email_verify['token_expiry_hours'] ?? 24 ); ?>" min="1" max="168" class="small-text">
4237 <?php esc_html_e( 'hours', 'vigilante' ); ?>
4238 </td>
4239 </tr>
4240 <tr>
4241 <th scope="row"><?php esc_html_e( 'Allow Resend', 'vigilante' ); ?></th>
4242 <td>
4243 <label>
4244 <input type="checkbox" name="user_security[email_verification][allow_resend]" value="1" <?php checked( ! empty( $email_verify['allow_resend'] ) ); ?>>
4245 <?php esc_html_e( 'Allow users to request a new verification email', 'vigilante' ); ?>
4246 </label>
4247 </td>
4248 </tr>
4249 <tr>
4250 <th scope="row"><?php esc_html_e( 'Auto-delete Unverified', 'vigilante' ); ?></th>
4251 <td>
4252 <input type="number" name="user_security[email_verification][auto_delete_days]" value="<?php echo esc_attr( $email_verify['auto_delete_days'] ?? 7 ); ?>" min="0" max="365" class="small-text">
4253 <?php esc_html_e( 'days (0 = never)', 'vigilante' ); ?>
4254 <p class="description"><?php esc_html_e( 'Automatically delete users who never verify their email.', 'vigilante' ); ?></p>
4255 </td>
4256 </tr>
4257 </table>
4258 </div>
4259
4260 <p class="submit vigilante-submit-buttons">
4261 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
4262 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
4263 </button>
4264 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
4265 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
4266 </button>
4267 </p>
4268 </form>
4269
4270 <!-- ============================================================
4271 TOOLS SECTION - Actions and utilities (no save button)
4272 ============================================================ -->
4273 <div class="vigilante-tools-section">
4274 <h2 class="vigilante-tools-header">
4275 <?php esc_html_e( 'User security tools', 'vigilante' ); ?>
4276 </h2>
4277
4278 <!-- Force Password Reset -->
4279 <div class="vigilante-tool-box">
4280 <h3><?php esc_html_e( 'Force password reset', 'vigilante' ); ?></h3>
4281 <p class="description"><?php esc_html_e( 'Force users to reset their password. Useful after a security incident. Users will receive an email with a reset link.', 'vigilante' ); ?></p>
4282
4283 <!-- Reset Specific Users -->
4284 <div class="vigilante-password-reset-box">
4285 <h4><?php esc_html_e( 'Reset specific users', 'vigilante' ); ?></h4>
4286
4287 <div class="vigilante-user-search-wrapper">
4288 <input type="text" id="vigilante-password-reset-search" class="regular-text" placeholder="<?php esc_attr_e( 'Search by username, email, or display name...', 'vigilante' ); ?>">
4289 <div id="vigilante-password-reset-results" class="vigilante-user-search-results" style="display: none;"></div>
4290 </div>
4291
4292 <div id="vigilante-password-reset-selected" class="vigilante-selected-users" style="display: none;">
4293 <strong><?php esc_html_e( 'Selected Users:', 'vigilante' ); ?></strong>
4294 <ul class="vigilante-selected-users-list"></ul>
4295 </div>
4296
4297 <p class="description" style="margin-top: 15px;">
4298 <span class="dashicons dashicons-email-alt" style="color: #2271b1;"></span>
4299 <?php esc_html_e( 'Selected users will receive an email with a password reset link.', 'vigilante' ); ?>
4300 </p>
4301
4302 <p class="submit">
4303 <button type="button" id="vigilante-reset-selected-users" class="button button-primary" disabled>
4304 <?php esc_html_e( 'Force Reset for Selected Users', 'vigilante' ); ?>
4305 </button>
4306 </p>
4307 </div>
4308
4309 <!-- Reset by Role -->
4310 <div class="vigilante-password-reset-box" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
4311 <h4><?php esc_html_e( 'Reset by role', 'vigilante' ); ?></h4>
4312 <p class="description"><?php esc_html_e( 'Select one or more roles to force a password reset for all users with those roles. Ideal for security incidents where you need to reset access quickly.', 'vigilante' ); ?></p>
4313
4314 <?php
4315 $wp_roles = wp_roles();
4316 $user_counts = count_users();
4317 $avail_roles = $user_counts['avail_roles'] ?? array();
4318 $current_user = wp_get_current_user();
4319 $current_roles = $current_user->roles;
4320 ?>
4321
4322 <fieldset class="vigilante-role-checkboxes" style="margin-top: 10px;">
4323 <?php foreach ( $wp_roles->roles as $role_slug => $role_data ) :
4324 $count = $avail_roles[ $role_slug ] ?? 0;
4325 if ( 0 === $count ) {
4326 continue;
4327 }
4328 $role_name = translate_user_role( $role_data['name'] );
4329 ?>
4330 <label style="display: block; margin-bottom: 6px;">
4331 <input type="checkbox"
4332 class="vigilante-reset-role-checkbox"
4333 value="<?php echo esc_attr( $role_slug ); ?>"
4334 data-count="<?php echo absint( $count ); ?>">
4335 <?php
4336 printf(
4337 /* translators: 1: Role name, 2: Number of users */
4338 '%1$s <span class="description">(%2$d)</span>',
4339 esc_html( $role_name ),
4340 absint( $count )
4341 );
4342 ?>
4343 <?php if ( in_array( $role_slug, $current_roles, true ) ) : ?>
4344 <em class="description"><?php esc_html_e( '(includes you)', 'vigilante' ); ?></em>
4345 <?php endif; ?>
4346 </label>
4347 <?php endforeach; ?>
4348 </fieldset>
4349
4350 <div id="vigilante-reset-role-summary" style="display: none; margin-top: 10px;">
4351 <p>
4352 <span class="dashicons dashicons-groups" style="color: #2271b1;"></span>
4353 <strong id="vigilante-reset-role-count">0</strong>
4354 <?php esc_html_e( 'user(s) will be affected.', 'vigilante' ); ?>
4355 </p>
4356 </div>
4357
4358 <div class="vigilante-password-reset-options" id="vigilante-reset-role-self-option" style="display: none; margin-top: 10px;">
4359 <label>
4360 <input type="checkbox" id="vigilante-reset-role-include-self" value="1">
4361 <?php esc_html_e( 'Include myself (your current session will end)', 'vigilante' ); ?>
4362 </label>
4363 </div>
4364
4365 <p class="submit">
4366 <button type="button" id="vigilante-reset-by-role" class="button button-primary" disabled>
4367 <?php esc_html_e( 'Force Reset for Selected Roles', 'vigilante' ); ?>
4368 </button>
4369 </p>
4370 </div>
4371
4372 <!-- Reset All Users -->
4373 <div class="vigilante-password-reset-box" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
4374 <h4><?php esc_html_e( 'Reset all users', 'vigilante' ); ?></h4>
4375
4376 <?php
4377 $total_users = count_users();
4378 $total_count = $total_users['total_users'];
4379 ?>
4380 <p>
4381 <?php
4382 printf(
4383 /* translators: %d: Number of users */
4384 esc_html__( 'This will affect %d user(s).', 'vigilante' ),
4385 absint( $total_count )
4386 );
4387 ?>
4388 </p>
4389
4390 <p class="description" style="color: #d63638;">
4391 <span class="dashicons dashicons-warning"></span>
4392 <?php esc_html_e( 'Warning: All users will receive a password reset email. On sites with many users, this could overwhelm your mail server.', 'vigilante' ); ?>
4393 </p>
4394
4395 <div class="vigilante-password-reset-options" style="margin-top: 10px;">
4396 <label>
4397 <input type="checkbox" id="vigilante-reset-all-include-self" value="1">
4398 <?php esc_html_e( 'Include myself (your current session will end)', 'vigilante' ); ?>
4399 </label>
4400 </div>
4401
4402 <p class="submit">
4403 <button type="button" id="vigilante-reset-all-users" class="button" style="color: #d63638; border-color: #d63638;">
4404 <?php esc_html_e( 'Force Reset for ALL Users', 'vigilante' ); ?>
4405 </button>
4406 </p>
4407 </div>
4408 </div>
4409
4410 <!-- Pending Registrations -->
4411 <?php
4412 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
4413 $pending_users = $user_security->get_pending_users();
4414 ?>
4415 <div class="vigilante-tool-box vigilante-pending-users-section">
4416 <h3>
4417 <?php esc_html_e( 'Pending registrations', 'vigilante' ); ?>
4418 <?php if ( count( $pending_users ) > 0 ) : ?>
4419 <span class="vigilante-badge vigilante-badge-warning"><?php echo esc_html( count( $pending_users ) ); ?></span>
4420 <?php endif; ?>
4421 </h3>
4422
4423 <?php if ( empty( $registration['enabled'] ) ) : ?>
4424 <p class="description">
4425 <span class="dashicons dashicons-info" style="color: #72aee6;"></span>
4426 <?php esc_html_e( 'Registration approval is disabled. Enable it in the settings above to require manual approval for new users.', 'vigilante' ); ?>
4427 </p>
4428 <?php elseif ( empty( $pending_users ) ) : ?>
4429 <div class="vigilante-no-lockouts">
4430 <span class="dashicons dashicons-yes-alt"></span>
4431 <p><?php esc_html_e( 'No pending registrations.', 'vigilante' ); ?></p>
4432 </div>
4433 <?php else : ?>
4434 <table class="wp-list-table widefat fixed striped vigilante-pending-users-table">
4435 <thead>
4436 <tr>
4437 <th><?php esc_html_e( 'User', 'vigilante' ); ?></th>
4438 <th><?php esc_html_e( 'Email', 'vigilante' ); ?></th>
4439 <th><?php esc_html_e( 'Registered', 'vigilante' ); ?></th>
4440 <th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
4441 </tr>
4442 </thead>
4443 <tbody>
4444 <?php foreach ( $pending_users as $pending_user ) :
4445 $pending_since = get_user_meta( $pending_user->ID, 'vigilante_pending_since', true );
4446 ?>
4447 <tr data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>">
4448 <td>
4449 <?php echo get_avatar( $pending_user->ID, 32 ); ?>
4450 <strong><?php echo esc_html( $pending_user->user_login ); ?></strong>
4451 </td>
4452 <td><?php echo esc_html( $pending_user->user_email ); ?></td>
4453 <td>
4454 <?php
4455 if ( $pending_since ) {
4456 /* translators: %s: Time ago */
4457 printf( esc_html__( '%s ago', 'vigilante' ), esc_html( human_time_diff( $pending_since ) ) );
4458 } else {
4459 echo esc_html( $pending_user->user_registered );
4460 }
4461 ?>
4462 </td>
4463 <td>
4464 <button type="button" class="button button-small vigilante-approve-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>">
4465 <?php esc_html_e( 'Approve', 'vigilante' ); ?>
4466 </button>
4467 <button type="button" class="button button-small vigilante-reject-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>" style="color: #d63638;">
4468 <?php esc_html_e( 'Reject', 'vigilante' ); ?>
4469 </button>
4470 </td>
4471 </tr>
4472 <?php endforeach; ?>
4473 </tbody>
4474 </table>
4475 <?php endif; ?>
4476 </div>
4477
4478 <!-- Active Sessions Management -->
4479 <div class="vigilante-tool-box vigilante-session-management-section">
4480 <h3><?php esc_html_e( 'Active sessions', 'vigilante' ); ?></h3>
4481 <p class="description"><?php esc_html_e( 'View and manage active login sessions. You can revoke sessions to force users to log in again.', 'vigilante' ); ?></p>
4482
4483 <!-- Current user sessions -->
4484 <h4><?php esc_html_e( 'Your sessions', 'vigilante' ); ?></h4>
4485 <?php
4486 $current_user_id = get_current_user_id();
4487 $my_sessions = $user_security->get_user_sessions( $current_user_id );
4488 $has_corrupted = $user_security->has_corrupted_sessions( $current_user_id );
4489 $raw_count = $user_security->get_raw_session_count( $current_user_id );
4490 ?>
4491
4492 <?php if ( $has_corrupted && $raw_count > 0 ) : ?>
4493 <div class="notice notice-warning inline" style="margin: 10px 0;">
4494 <p>
4495 <span class="dashicons dashicons-warning" style="color: #dba617;"></span>
4496 <?php esc_html_e( 'Some session data is corrupted and cannot be displayed. Use "Revoke All Other Sessions" to clean up, then log out and log in again to fix this.', 'vigilante' ); ?>
4497 </p>
4498 </div>
4499 <?php endif; ?>
4500
4501 <?php if ( empty( $my_sessions ) ) : ?>
4502 <p class="description"><?php esc_html_e( 'No active sessions found.', 'vigilante' ); ?></p>
4503 <?php if ( $has_corrupted ) : ?>
4504 <p style="margin-top: 10px;">
4505 <button type="button" class="button vigilante-revoke-other-sessions" data-user-id="<?php echo esc_attr( $current_user_id ); ?>">
4506 <?php esc_html_e( 'Clean Up Corrupted Sessions', 'vigilante' ); ?>
4507 </button>
4508 </p>
4509 <?php endif; ?>
4510 <?php else : ?>
4511 <div class="vigilante-paginated-section">
4512 <div class="vigilante-fi-pagination-wrap"></div>
4513 <table class="wp-list-table widefat fixed striped vigilante-sessions-table vigilante-fi-paginated">
4514 <thead>
4515 <tr>
4516 <th><?php esc_html_e( 'Browser', 'vigilante' ); ?></th>
4517 <th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th>
4518 <th><?php esc_html_e( 'Login Time', 'vigilante' ); ?></th>
4519 <th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
4520 </tr>
4521 </thead>
4522 <tbody>
4523 <?php foreach ( $my_sessions as $session ) : ?>
4524 <tr data-token="<?php echo esc_attr( $session['token_hash'] ); ?>">
4525 <td><?php echo esc_html( $session['browser'] ); ?></td>
4526 <td><code><?php echo esc_html( $session['ip'] ); ?></code></td>
4527 <td>
4528 <?php
4529 if ( $session['login'] ) {
4530 /* translators: %s: Time ago */
4531 printf( esc_html__( '%s ago', 'vigilante' ), esc_html( human_time_diff( $session['login'] ) ) );
4532 } else {
4533 esc_html_e( 'Unknown', 'vigilante' );
4534 }
4535 ?>
4536 </td>
4537 <td>
4538 <?php if ( ! $session['is_current'] ) : ?>
4539 <button type="button" class="button button-small vigilante-revoke-session" data-user-id="<?php echo esc_attr( $current_user_id ); ?>" data-token="<?php echo esc_attr( $session['token_hash'] ); ?>">
4540 <?php esc_html_e( 'Revoke', 'vigilante' ); ?>
4541 </button>
4542 <?php else : ?>
4543 <span class="description"><?php esc_html_e( 'Current session', 'vigilante' ); ?></span>
4544 <?php endif; ?>
4545 </td>
4546 </tr>
4547 <?php endforeach; ?>
4548 </tbody>
4549 </table>
4550 </div>
4551
4552 <?php if ( count( $my_sessions ) > 1 || $has_corrupted ) : ?>
4553 <p style="margin-top: 10px;">
4554 <button type="button" class="button vigilante-revoke-other-sessions" data-user-id="<?php echo esc_attr( $current_user_id ); ?>">
4555 <?php esc_html_e( 'Revoke All Other Sessions', 'vigilante' ); ?>
4556 </button>
4557 </p>
4558 <?php endif; ?>
4559 <?php endif; ?>
4560
4561 <!-- Search user sessions (admin only) -->
4562 <h4 style="margin-top: 30px;"><?php esc_html_e( 'Manage user sessions', 'vigilante' ); ?></h4>
4563 <p class="description"><?php esc_html_e( 'Search for a user to view and manage their sessions.', 'vigilante' ); ?></p>
4564
4565 <div class="vigilante-user-search-wrapper" style="margin-top: 10px;">
4566 <input type="text" id="vigilante-session-user-search" class="regular-text" placeholder="<?php esc_attr_e( 'Search by username or email...', 'vigilante' ); ?>">
4567 <div id="vigilante-session-search-results" class="vigilante-user-search-results" style="display: none;"></div>
4568 </div>
4569
4570 <div id="vigilante-user-sessions-container" style="display: none; margin-top: 20px;">
4571 <h4 id="vigilante-sessions-user-name"></h4>
4572 <table class="wp-list-table widefat fixed striped vigilante-sessions-table">
4573 <thead>
4574 <tr>
4575 <th><?php esc_html_e( 'Browser', 'vigilante' ); ?></th>
4576 <th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th>
4577 <th><?php esc_html_e( 'Login Time', 'vigilante' ); ?></th>
4578 <th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
4579 </tr>
4580 </thead>
4581 <tbody id="vigilante-user-sessions-list">
4582 </tbody>
4583 </table>
4584 <p style="margin-top: 10px;">
4585 <button type="button" class="button vigilante-revoke-all-user-sessions" style="color: #d63638;">
4586 <?php esc_html_e( 'Revoke All Sessions', 'vigilante' ); ?>
4587 </button>
4588 </p>
4589 </div>
4590 </div>
4591 </div>
4592 <?php
4593 }
4594
4595 /**
4596 * Render WordPress Hardening tab
4597 */
4598 private function render_tab_wp_hardening() {
4599 $is_disabled = $this->render_module_disabled_notice( 'wp_hardening' );
4600 $options = $this->settings->get_section( 'wp_hardening' );
4601 ?>
4602 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="wp_hardening" <?php echo $is_disabled ? 'inert' : ''; ?>>
4603 <!-- Database Hardening (outside form save flow - uses its own AJAX action) -->
4604 <div id="vigilante-section-hardening-database" class="vigilante-settings-section">
4605 <h2>
4606 <?php esc_html_e( 'Database Hardening', 'vigilante' ); ?>
4607 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
4608 <span class="vigilante-method-badge config"><?php esc_html_e( 'WP-CONFIG', 'vigilante' ); ?></span>
4609 </h2>
4610 <p><?php esc_html_e( 'Change the database table prefix to prevent SQL injection attacks that target default WordPress tables.', 'vigilante' ); ?></p>
4611
4612 <?php
4613 $db_prefix = new Vigilante_Database_Prefix();
4614 $current_prefix = $db_prefix->get_current_prefix();
4615 $is_default = $db_prefix->is_default_prefix();
4616 ?>
4617
4618 <table class="form-table">
4619 <tr>
4620 <th scope="row"><?php esc_html_e( 'Current prefix', 'vigilante' ); ?></th>
4621 <td>
4622 <code class="vigilante-db-current-prefix"><?php echo esc_html( $current_prefix ); ?></code>
4623 <?php if ( $is_default ) : ?>
4624 <span class="vigilante-inline-warning">
4625 <span class="dashicons dashicons-warning"></span>
4626 <?php esc_html_e( 'Default prefix detected. Changing it adds a layer of protection against automated SQL injection attacks.', 'vigilante' ); ?>
4627 </span>
4628 <?php else : ?>
4629 <span class="vigilante-inline-ok">
4630 <span class="dashicons dashicons-yes-alt"></span>
4631 <?php esc_html_e( 'Custom prefix in use.', 'vigilante' ); ?>
4632 </span>
4633 <?php endif; ?>
4634 </td>
4635 </tr>
4636 <tr>
4637 <th scope="row"><?php esc_html_e( 'New prefix', 'vigilante' ); ?></th>
4638 <td>
4639 <div class="vigilante-db-prefix-row">
4640 <code class="vigilante-db-new-prefix" id="vigilante-new-prefix"><?php echo esc_html( $db_prefix->generate_prefix() ); ?></code>
4641 <button type="button" class="button button-small vigilante-db-regenerate-prefix" title="<?php esc_attr_e( 'Generate new prefix', 'vigilante' ); ?>">
4642 <span class="dashicons dashicons-update"></span>
4643 </button>
4644 </div>
4645 </td>
4646 </tr>
4647 <tr>
4648 <th scope="row"></th>
4649 <td>
4650 <div class="vigilante-db-prefix-confirm">
4651 <label>
4652 <input type="checkbox" id="vigilante-prefix-backup-confirm">
4653 <?php esc_html_e( 'I understand this operation is irreversible and I have a current database backup', 'vigilante' ); ?>
4654 </label>
4655 <p class="description">
4656 <?php
4657 printf(
4658 /* translators: %s: Link to tools tab */
4659 esc_html__( 'Need a backup? %s first.', 'vigilante' ),
4660 '<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Download a database backup', 'vigilante' ) . '</a>'
4661 );
4662 ?>
4663 </p>
4664 </div>
4665 <button type="button" class="button button-primary vigilante-db-change-prefix" disabled data-original-text="<?php esc_attr_e( 'Change Database Prefix', 'vigilante' ); ?>">
4666 <?php esc_html_e( 'Change Database Prefix', 'vigilante' ); ?>
4667 </button>
4668 </td>
4669 </tr>
4670 </table>
4671 </div>
4672
4673 <!-- wp-config Security -->
4674 <div id="vigilante-section-hardening-wpconfig" class="vigilante-settings-section">
4675 <h2>
4676 <?php esc_html_e( 'wp-config.php Security', 'vigilante' ); ?>
4677 <span class="vigilante-method-badge config"><?php esc_html_e( 'WP-CONFIG', 'vigilante' ); ?></span>
4678 </h2>
4679 <p><?php esc_html_e( 'Security constants added directly to wp-config.php file.', 'vigilante' ); ?></p>
4680
4681 <table class="form-table">
4682 <tr>
4683 <th scope="row"><?php esc_html_e( 'Disable File Editor', 'vigilante' ); ?></th>
4684 <td>
4685 <label>
4686 <input type="checkbox" name="wp_hardening[disallow_file_edit]" value="1" <?php checked( ! empty( $options['disallow_file_edit'] ) ); ?>>
4687 <?php esc_html_e( 'Disable plugin and theme editor in admin (DISALLOW_FILE_EDIT)', 'vigilante' ); ?>
4688 </label>
4689 </td>
4690 </tr>
4691 <tr>
4692 <th scope="row"><?php esc_html_e( 'Disable File Modifications', 'vigilante' ); ?></th>
4693 <td>
4694 <label>
4695 <input type="checkbox" name="wp_hardening[disallow_file_mods]" value="1" <?php checked( ! empty( $options['disallow_file_mods'] ) ); ?>>
4696 <?php esc_html_e( 'Disable all file modifications including updates (DISALLOW_FILE_MODS)', 'vigilante' ); ?>
4697 </label>
4698 <p class="description"><?php esc_html_e( '&#9888; Warning: This prevents automatic updates.', 'vigilante' ); ?></p>
4699 </td>
4700 </tr>
4701 <tr id="field-force-ssl-admin">
4702 <th scope="row"><?php esc_html_e( 'Force SSL Admin', 'vigilante' ); ?></th>
4703 <td>
4704 <label>
4705 <input type="checkbox" name="wp_hardening[force_ssl_admin]" value="1" <?php checked( ! empty( $options['force_ssl_admin'] ) ); ?>>
4706 <?php esc_html_e( 'Force HTTPS for admin area (FORCE_SSL_ADMIN)', 'vigilante' ); ?>
4707 </label>
4708 <p class="description"><?php esc_html_e( '&#9888; Warning: Only enable if your site fully supports HTTPS.', 'vigilante' ); ?></p>
4709 </td>
4710 </tr>
4711 <tr id="field-wp-debug">
4712 <th scope="row"><?php esc_html_e( 'Hide PHP errors from visitors', 'vigilante' ); ?></th>
4713 <td>
4714 <label>
4715 <input type="checkbox" name="wp_hardening[wp_debug]" value="1" <?php checked( ! empty( $options['wp_debug'] ) ); ?>>
4716 <?php esc_html_e( 'Prevents PHP errors and warnings from being displayed publicly. Also avoids exposing a debug.log file in wp-content/ that could leak paths and code. Uncheck only on development or staging sites.', 'vigilante' ); ?>
4717 </label>
4718 </td>
4719 </tr>
4720 <tr id="field-disable-wp-cron">
4721 <th scope="row"><?php esc_html_e( 'Disable WP Cron', 'vigilante' ); ?></th>
4722 <td>
4723 <label>
4724 <input type="checkbox" name="wp_hardening[disable_wp_cron]" value="1" <?php checked( ! empty( $options['disable_wp_cron'] ) ); ?>>
4725 <?php esc_html_e( 'Disable WordPress\'s page-view cron trigger (DISABLE_WP_CRON)', 'vigilante' ); ?>
4726 </label>
4727 <p class="description"><?php
4728 printf(
4729 /* translators: 1: opening <strong>, 2: closing </strong>, 3: opening <code>, 4: closing </code> */
4730 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 &rarr; File Protection.', 'vigilante' ),
4731 '<strong>',
4732 '</strong>',
4733 '<code>',
4734 '</code>'
4735 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded.
4736 ?></p>
4737 </td>
4738 </tr>
4739 </table>
4740 </div>
4741
4742 <!-- Comment Security -->
4743 <div id="vigilante-section-hardening-comments" class="vigilante-settings-section">
4744 <h2>
4745 <?php esc_html_e( 'Comment Security', 'vigilante' ); ?>
4746 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4747 <span class="vigilante-method-badge settings"><?php esc_html_e( 'Settings', 'vigilante' ); ?></span>
4748 </h2>
4749 <p><?php esc_html_e( 'Comment protection using WordPress settings and PHP hooks.', 'vigilante' ); ?></p>
4750
4751 <table class="form-table">
4752 <tr>
4753 <th scope="row"><?php esc_html_e( 'Disable Pingbacks', 'vigilante' ); ?></th>
4754 <td>
4755 <label>
4756 <input type="checkbox" name="wp_hardening[disable_pingbacks]" value="1" <?php checked( ! empty( $options['disable_pingbacks'] ) ); ?>>
4757 <?php esc_html_e( 'Disable pingbacks (commonly exploited for DDoS)', 'vigilante' ); ?>
4758 </label>
4759 </td>
4760 </tr>
4761 <tr>
4762 <th scope="row"><?php esc_html_e( 'Disable Trackbacks', 'vigilante' ); ?></th>
4763 <td>
4764 <label>
4765 <input type="checkbox" name="wp_hardening[disable_trackbacks]" value="1" <?php checked( ! empty( $options['disable_trackbacks'] ) ); ?>>
4766 <?php esc_html_e( 'Disable trackbacks (rarely used legitimately)', 'vigilante' ); ?>
4767 </label>
4768 </td>
4769 </tr>
4770 <tr>
4771 <th scope="row"><?php esc_html_e( 'Require Moderation', 'vigilante' ); ?></th>
4772 <td>
4773 <label>
4774 <input type="checkbox" name="wp_hardening[require_comment_moderation]" value="1" <?php checked( ! empty( $options['require_comment_moderation'] ) ); ?>>
4775 <?php esc_html_e( 'All comments must be manually approved', 'vigilante' ); ?>
4776 </label>
4777 </td>
4778 </tr>
4779 <tr>
4780 <th scope="row"><?php esc_html_e( 'Close Old Comments', 'vigilante' ); ?></th>
4781 <td>
4782 <label>
4783 <input type="checkbox" name="wp_hardening[close_old_comments]" value="1" <?php checked( ! empty( $options['close_old_comments'] ) ); ?>>
4784 <?php esc_html_e( 'Automatically close comments on old posts after', 'vigilante' ); ?>
4785 </label>
4786 <input type="number" name="wp_hardening[close_comments_after_days]" value="<?php echo esc_attr( $options['close_comments_after_days'] ?? 30 ); ?>" min="1" max="365" class="small-text">
4787 <?php esc_html_e( 'days', 'vigilante' ); ?>
4788 </td>
4789 </tr>
4790 <tr>
4791 <th scope="row"><?php esc_html_e( 'Honeypot Protection', 'vigilante' ); ?></th>
4792 <td>
4793 <label>
4794 <input type="checkbox" name="wp_hardening[honeypot_comments]" value="1" <?php checked( ! empty( $options['honeypot_comments'] ) ); ?>>
4795 <?php esc_html_e( 'Add hidden honeypot field to catch bots', 'vigilante' ); ?>
4796 </label>
4797 </td>
4798 </tr>
4799 </table>
4800 </div>
4801
4802 <!-- Head Cleaner -->
4803 <div id="vigilante-section-hardening-headers" class="vigilante-settings-section">
4804 <h2>
4805 <?php esc_html_e( 'Header Cleanup', 'vigilante' ); ?>
4806 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4807 </h2>
4808 <p><?php esc_html_e( 'Remove meta tags from HTML head.', 'vigilante' ); ?></p>
4809
4810 <table class="form-table">
4811 <tr>
4812 <th scope="row"><?php esc_html_e( 'Remove Generator', 'vigilante' ); ?></th>
4813 <td>
4814 <label>
4815 <input type="checkbox" name="wp_hardening[remove_wp_generator]" value="1" <?php checked( ! empty( $options['remove_wp_generator'] ) ); ?>>
4816 <?php esc_html_e( 'Remove WordPress version from HTML head', 'vigilante' ); ?>
4817 </label>
4818 </td>
4819 </tr>
4820 <tr id="field-remove-wp-version-assets">
4821 <th scope="row"><?php esc_html_e( 'Remove version from assets', 'vigilante' ); ?></th>
4822 <td>
4823 <label>
4824 <input type="checkbox" name="wp_hardening[remove_wp_version_assets]" value="1" <?php checked( ! empty( $options['remove_wp_version_assets'] ) ); ?>>
4825 <?php esc_html_e( 'Remove WordPress version from script/style URLs (?ver=)', 'vigilante' ); ?>
4826 </label>
4827 <p class="description"><?php esc_html_e( 'Hides the exact WordPress version that would otherwise leak in every enqueued asset URL. Versions added by plugins or themes are kept untouched.', 'vigilante' ); ?></p>
4828 </td>
4829 </tr>
4830 <tr>
4831 <th scope="row"><?php esc_html_e( 'Remove RSD Link', 'vigilante' ); ?></th>
4832 <td>
4833 <label>
4834 <input type="checkbox" name="wp_hardening[remove_rsd_link]" value="1" <?php checked( ! empty( $options['remove_rsd_link'] ) ); ?>>
4835 <?php esc_html_e( 'Remove Really Simple Discovery link', 'vigilante' ); ?>
4836 </label>
4837 </td>
4838 </tr>
4839 <tr>
4840 <th scope="row"><?php esc_html_e( 'Remove WLW Manifest', 'vigilante' ); ?></th>
4841 <td>
4842 <label>
4843 <input type="checkbox" name="wp_hardening[remove_wlw_manifest]" value="1" <?php checked( ! empty( $options['remove_wlw_manifest'] ) ); ?>>
4844 <?php esc_html_e( 'Remove Windows Live Writer manifest link', 'vigilante' ); ?>
4845 </label>
4846 </td>
4847 </tr>
4848 <tr>
4849 <th scope="row"><?php esc_html_e( 'Remove Shortlink', 'vigilante' ); ?></th>
4850 <td>
4851 <label>
4852 <input type="checkbox" name="wp_hardening[remove_shortlink]" value="1" <?php checked( ! empty( $options['remove_shortlink'] ) ); ?>>
4853 <?php esc_html_e( 'Remove shortlink tag from header', 'vigilante' ); ?>
4854 </label>
4855 </td>
4856 </tr>
4857 <tr>
4858 <th scope="row"><?php esc_html_e( 'Remove REST API Link', 'vigilante' ); ?></th>
4859 <td>
4860 <label>
4861 <input type="checkbox" name="wp_hardening[remove_rest_api_link]" value="1" <?php checked( ! empty( $options['remove_rest_api_link'] ) ); ?>>
4862 <?php esc_html_e( 'Remove REST API discovery link from header', 'vigilante' ); ?>
4863 </label>
4864 <p class="description"><?php esc_html_e( '&#9888; Notice: Some plugins may need this link.', 'vigilante' ); ?></p>
4865 </td>
4866 </tr>
4867 </table>
4868 </div>
4869
4870 <!-- Feed Manager -->
4871 <div id="vigilante-section-hardening-rss" class="vigilante-settings-section">
4872 <h2>
4873 <?php esc_html_e( 'RSS Feed Settings', 'vigilante' ); ?>
4874 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4875 </h2>
4876 <p><?php esc_html_e( 'Control RSS/Atom feeds.', 'vigilante' ); ?></p>
4877
4878 <table class="form-table">
4879 <tr>
4880 <th scope="row"><?php esc_html_e( 'Disable Feeds', 'vigilante' ); ?></th>
4881 <td>
4882 <label>
4883 <input type="checkbox" name="wp_hardening[disable_feeds]" value="1" <?php checked( ! empty( $options['disable_feeds'] ) ); ?>>
4884 <?php esc_html_e( 'Completely disable RSS/Atom feeds', 'vigilante' ); ?>
4885 </label>
4886 </td>
4887 </tr>
4888 <tr>
4889 <th scope="row"><?php esc_html_e( 'Disable If No Content', 'vigilante' ); ?></th>
4890 <td>
4891 <label>
4892 <input type="checkbox" name="wp_hardening[disable_if_no_content]" value="1" <?php checked( ! empty( $options['disable_if_no_content'] ) ); ?>>
4893 <?php esc_html_e( 'Only disable feeds if site has no published posts', 'vigilante' ); ?>
4894 </label>
4895 </td>
4896 </tr>
4897 <tr>
4898 <th scope="row"><?php esc_html_e( 'Remove Feed Version', 'vigilante' ); ?></th>
4899 <td>
4900 <label>
4901 <input type="checkbox" name="wp_hardening[remove_feed_version]" value="1" <?php checked( ! empty( $options['remove_feed_version'] ) ); ?>>
4902 <?php esc_html_e( 'Remove WordPress version from feed generator tag', 'vigilante' ); ?>
4903 </label>
4904 </td>
4905 </tr>
4906 </table>
4907 </div>
4908
4909 <p class="submit vigilante-submit-buttons">
4910 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
4911 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
4912 </button>
4913 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
4914 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
4915 </button>
4916 </p>
4917 </form>
4918 <?php
4919 }
4920
4921 /**
4922 * Render activity log tab
4923 */
4924 private function render_tab_activity_log() {
4925 $is_disabled = $this->render_module_disabled_notice( 'activity_log' );
4926 $options = $this->settings->get_section( 'activity_log' );
4927 ?>
4928 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="activity_log" <?php echo $is_disabled ? 'inert' : ''; ?>>
4929 <div id="vigilante-section-audit-settings" class="vigilante-settings-section">
4930 <h2>
4931 <?php esc_html_e( 'Security Audit Settings', 'vigilante' ); ?>
4932 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4933 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
4934 </h2>
4935 <p><?php esc_html_e( 'Security event logging and auditing.', 'vigilante' ); ?></p>
4936
4937 <table class="form-table">
4938 <tr>
4939 <th scope="row"><?php esc_html_e( 'Retention', 'vigilante' ); ?></th>
4940 <td>
4941 <input type="number" name="activity_log[retention_days]" value="<?php echo esc_attr( $options['retention_days'] ?? 30 ); ?>" min="7" max="365" class="small-text">
4942 <?php esc_html_e( 'days', 'vigilante' ); ?>
4943 &nbsp;&nbsp;
4944 <input type="number" name="activity_log[max_entries]" value="<?php echo esc_attr( $options['max_entries'] ?? 10000 ); ?>" min="100" max="100000" step="100" class="small-text">
4945 <?php esc_html_e( 'max entries', 'vigilante' ); ?>
4946 <p class="description"><?php esc_html_e( 'Whichever limit is reached first takes effect. Changes apply immediately on save; daily maintenance also enforces these limits automatically.', 'vigilante' ); ?></p>
4947 </td>
4948 </tr>
4949 <tr>
4950 <th scope="row"><?php esc_html_e( 'Events to Log', 'vigilante' ); ?></th>
4951 <td>
4952 <fieldset style="display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)); gap:6px 24px; max-width:600px;">
4953 <label><input type="checkbox" name="activity_log[log_logins]" value="1" <?php checked( ! empty( $options['log_logins'] ) ); ?>> <?php esc_html_e( 'Successful logins', 'vigilante' ); ?></label>
4954 <label><input type="checkbox" name="activity_log[log_failed_logins]" value="1" <?php checked( ! empty( $options['log_failed_logins'] ) ); ?>> <?php esc_html_e( 'Failed login attempts', 'vigilante' ); ?></label>
4955 <label><input type="checkbox" name="activity_log[log_user_changes]" value="1" <?php checked( ! empty( $options['log_user_changes'] ) ); ?>> <?php esc_html_e( 'User changes', 'vigilante' ); ?></label>
4956 <label><input type="checkbox" name="activity_log[log_post_changes]" value="1" <?php checked( ! empty( $options['log_post_changes'] ) ); ?>> <?php esc_html_e( 'Content changes', 'vigilante' ); ?></label>
4957 <label><input type="checkbox" name="activity_log[log_plugin_changes]" value="1" <?php checked( ! empty( $options['log_plugin_changes'] ) ); ?>> <?php esc_html_e( 'Plugin changes', 'vigilante' ); ?></label>
4958 <label><input type="checkbox" name="activity_log[log_theme_changes]" value="1" <?php checked( ! empty( $options['log_theme_changes'] ) ); ?>> <?php esc_html_e( 'Theme changes', 'vigilante' ); ?></label>
4959 <label><input type="checkbox" name="activity_log[log_comments]" value="1" <?php checked( ! empty( $options['log_comments'] ) ); ?>> <?php esc_html_e( 'Comment changes', 'vigilante' ); ?></label>
4960 <label><input type="checkbox" name="activity_log[log_media]" value="1" <?php checked( ! empty( $options['log_media'] ) ); ?>> <?php esc_html_e( 'Media uploads/deletions', 'vigilante' ); ?></label>
4961 <label><input type="checkbox" name="activity_log[log_file_changes]" value="1" <?php checked( ! empty( $options['log_file_changes'] ) ); ?>> <?php esc_html_e( 'File integrity events', 'vigilante' ); ?></label>
4962 <label><input type="checkbox" name="activity_log[log_option_changes]" value="1" <?php checked( ! empty( $options['log_option_changes'] ) ); ?>> <?php esc_html_e( 'WordPress option changes', 'vigilante' ); ?></label>
4963 </fieldset>
4964 <div class="notice notice-info inline" style="margin:10px 0 0;padding:8px 12px;">
4965 <p style="margin:0;">
4966 <?php esc_html_e( 'Firewall blocks, security events, and Vigilant settings changes are always logged regardless of the above selections.', 'vigilante' ); ?>
4967 </p>
4968 </div>
4969 </td>
4970 </tr>
4971 <tr>
4972 <th scope="row"><?php esc_html_e( 'Option Tracking', 'vigilante' ); ?></th>
4973 <td>
4974 <p class="description" style="margin-top:0;"><?php esc_html_e( 'When "WordPress option changes" is enabled, Vigilant tracks ~30 core WordPress settings (site URL, admin email, registration, active plugins, theme, comments, privacy, etc.). Use the field below to track additional options from other plugins.', 'vigilante' ); ?></p>
4975 <br>
4976 <label><?php esc_html_e( 'Additional options to track:', 'vigilante' ); ?></label><br>
4977 <textarea name="activity_log[tracked_options]" rows="3" cols="50" class="regular-text code" placeholder="woocommerce_&#10;seopress_&#10;wpforms_"><?php echo esc_textarea( implode( "\n", $options['tracked_options'] ?? array() ) ); ?></textarea>
4978 <p class="description"><?php esc_html_e( 'One option name per line. Use a trailing underscore to match all options with that prefix (e.g. "woocommerce_" tracks all WooCommerce settings).', 'vigilante' ); ?></p>
4979 </td>
4980 </tr>
4981 <tr>
4982 <th scope="row"><?php esc_html_e( 'Exclusions', 'vigilante' ); ?></th>
4983 <td>
4984 <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:16px; max-width:600px;">
4985 <div>
4986 <label><?php esc_html_e( 'Excluded user IDs:', 'vigilante' ); ?></label><br>
4987 <textarea name="activity_log[excluded_users]" rows="3" cols="25"><?php echo esc_textarea( implode( "\n", $options['excluded_users'] ?? array() ) ); ?></textarea>
4988 <p class="description"><?php esc_html_e( 'One user ID per line. Actions by these users will not be logged.', 'vigilante' ); ?></p>
4989 </div>
4990 <div>
4991 <label><?php esc_html_e( 'Excluded IPs:', 'vigilante' ); ?></label><br>
4992 <textarea name="activity_log[excluded_ips]" rows="3" cols="25"><?php echo esc_textarea( implode( "\n", $options['excluded_ips'] ?? array() ) ); ?></textarea>
4993 <p class="description"><?php esc_html_e( 'One IP per line. Requests from these IPs will not be logged.', 'vigilante' ); ?></p>
4994 </div>
4995 </div>
4996 </td>
4997 </tr>
4998 </table>
4999 </div>
5000
5001 <p class="submit vigilante-submit-buttons">
5002 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
5003 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
5004 </button>
5005 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
5006 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
5007 </button>
5008 </p>
5009 </form>
5010
5011 <?php
5012 // Audit Alerts — alerting layer on top of Security Audit. Its own
5013 // settings section and form, rendered right after the logging settings
5014 // so the tab reads as "log this, exclude that, and alert me about this".
5015 $alerts = $this->settings->get_section( 'audit_alerts' );
5016 $immediate = isset( $alerts['immediate'] ) ? $alerts['immediate'] : array();
5017 $threshold = isset( $alerts['threshold'] ) ? $alerts['threshold'] : array();
5018 $alert_severity = isset( $immediate['min_severity'] ) ? $immediate['min_severity'] : 'critical';
5019 $alert_window = isset( $threshold['window'] ) ? $threshold['window'] : '1h';
5020 $threshold_cats = isset( $threshold['categories'] ) ? (array) $threshold['categories'] : array();
5021 $cat_labels = Vigilante_Audit_Alerts::category_labels();
5022 ?>
5023 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="audit_alerts" <?php echo $is_disabled ? 'inert' : ''; ?>>
5024 <div id="vigilante-section-audit-alerts" class="vigilante-settings-section">
5025 <h2>
5026 <?php esc_html_e( 'Audit Alerts', 'vigilante' ); ?>
5027 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5028 </h2>
5029 <p><?php esc_html_e( 'Get an email when the events above point to something worth your attention. Both alert types are off by default.', 'vigilante' ); ?></p>
5030
5031 <table class="form-table">
5032 <tr id="field-audit-alerts-immediate">
5033 <th scope="row"><?php esc_html_e( 'Immediate alerts', 'vigilante' ); ?></th>
5034 <td>
5035 <label>
5036 <input type="checkbox" name="audit_alerts[immediate][enabled]" value="1" <?php checked( ! empty( $immediate['enabled'] ) ); ?>>
5037 <?php esc_html_e( 'Email me as soon as a serious event is logged', 'vigilante' ); ?>
5038 </label>
5039 <p class="description"><?php esc_html_e( 'Sends one email per event type, then waits for the cooldown below before repeating, so a burst of the same event is a single notice.', 'vigilante' ); ?></p>
5040 </td>
5041 </tr>
5042 <tr>
5043 <th scope="row"><?php esc_html_e( 'Alert on severity', 'vigilante' ); ?></th>
5044 <td>
5045 <select name="audit_alerts[immediate][min_severity]">
5046 <option value="critical" <?php selected( $alert_severity, 'critical' ); ?>><?php esc_html_e( 'Critical only (recommended)', 'vigilante' ); ?></option>
5047 <option value="warning" <?php selected( $alert_severity, 'warning' ); ?>><?php esc_html_e( 'Warning and Critical', 'vigilante' ); ?></option>
5048 </select>
5049 <p class="description"><?php esc_html_e( 'A new administrator, a closed plugin or a privilege escalation are all logged as Critical, so "Critical only" already covers them.', 'vigilante' ); ?></p>
5050 </td>
5051 </tr>
5052 <tr id="field-audit-alerts-threshold">
5053 <th scope="row"><?php esc_html_e( 'Threshold alerts', 'vigilante' ); ?></th>
5054 <td>
5055 <label>
5056 <input type="checkbox" name="audit_alerts[threshold][enabled]" value="1" <?php checked( ! empty( $threshold['enabled'] ) ); ?>>
5057 <?php esc_html_e( 'Email me when a category spikes within a time window', 'vigilante' ); ?>
5058 </label>
5059 <p class="description"><?php esc_html_e( 'Catches an attack in progress, e.g. hundreds of firewall blocks or login failures in an hour.', 'vigilante' ); ?></p>
5060 </td>
5061 </tr>
5062 <tr>
5063 <th scope="row"><?php esc_html_e( 'Time window', 'vigilante' ); ?></th>
5064 <td>
5065 <select name="audit_alerts[threshold][window]">
5066 <option value="30m" <?php selected( $alert_window, '30m' ); ?>><?php esc_html_e( '30 minutes', 'vigilante' ); ?></option>
5067 <option value="1h" <?php selected( $alert_window, '1h' ); ?>><?php esc_html_e( '1 hour', 'vigilante' ); ?></option>
5068 <option value="6h" <?php selected( $alert_window, '6h' ); ?>><?php esc_html_e( '6 hours', 'vigilante' ); ?></option>
5069 <option value="24h" <?php selected( $alert_window, '24h' ); ?>><?php esc_html_e( '24 hours', 'vigilante' ); ?></option>
5070 </select>
5071 <p class="description"><?php esc_html_e( 'How far back Vigilant looks when counting events. For example, "1 hour" means "more than the number below within the last hour".', 'vigilante' ); ?></p>
5072 </td>
5073 </tr>
5074 <tr>
5075 <th scope="row"><?php esc_html_e( 'Thresholds per category', 'vigilante' ); ?></th>
5076 <td>
5077 <fieldset style="display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:8px 24px; max-width:760px;">
5078 <?php
5079 foreach ( $cat_labels as $cat_slug => $cat_label ) :
5080 $cat_value = isset( $threshold_cats[ $cat_slug ] ) ? (int) $threshold_cats[ $cat_slug ] : 0;
5081 ?>
5082 <label style="display:flex;align-items:center;gap:8px;justify-content:space-between;">
5083 <span><?php echo esc_html( $cat_label ); ?></span>
5084 <input type="number" name="audit_alerts[threshold][categories][<?php echo esc_attr( $cat_slug ); ?>]" value="<?php echo esc_attr( $cat_value ); ?>" min="0" max="100000" step="1" class="small-text">
5085 </label>
5086 <?php endforeach; ?>
5087 </fieldset>
5088 <p class="description"><?php esc_html_e( 'Number of warning/critical events in the window that triggers an alert. 0 disables that category. Routine info-level activity (normal logins, edits) is not counted.', 'vigilante' ); ?></p>
5089 </td>
5090 </tr>
5091 <tr>
5092 <th scope="row"><?php esc_html_e( "Don't repeat alerts", 'vigilante' ); ?></th>
5093 <td>
5094 <input type="number" name="audit_alerts[cooldown_minutes]" value="<?php echo esc_attr( isset( $alerts['cooldown_minutes'] ) ? (int) $alerts['cooldown_minutes'] : 60 ); ?>" min="0" max="1440" class="small-text">
5095 <?php esc_html_e( 'minutes', 'vigilante' ); ?>
5096 <p class="description"><?php esc_html_e( 'After an alert, Vigilant waits this long before sending another about the same thing: the same event type for immediate alerts, or the same category for threshold alerts. This prevents a flood during a sustained attack. Applies to both alert types above.', 'vigilante' ); ?></p>
5097 </td>
5098 </tr>
5099
5100 <tr>
5101 <th scope="row"><?php esc_html_e( 'Recipients', 'vigilante' ); ?></th>
5102 <td>
5103 <p class="description" style="margin-top:0;">
5104 <?php
5105 printf(
5106 /* translators: %s: Link to notification settings */
5107 esc_html__( 'Alerts go to the recipients configured in %s.', 'vigilante' ),
5108 '<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>'
5109 );
5110 ?>
5111 </p>
5112 <p style="margin:8px 0 0;">
5113 <button type="button" class="button vigilante-test-email-btn" data-original-text="<?php esc_attr_e( 'Send test email', 'vigilante' ); ?>">
5114 <?php esc_html_e( 'Send test email', 'vigilante' ); ?>
5115 </button>
5116 <span class="vigilante-test-email-result" style="margin-left:8px;"></span>
5117 </p>
5118 <p class="description"><?php esc_html_e( 'Heads up: some events (a new admin, a closed plugin) already send their own email from other modules. Enabling alerts for them here too may produce two notices until notifications are unified.', 'vigilante' ); ?></p>
5119 </td>
5120 </tr>
5121 </table>
5122 </div>
5123
5124 <p class="submit vigilante-submit-buttons">
5125 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
5126 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
5127 </button>
5128 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
5129 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
5130 </button>
5131 </p>
5132 </form>
5133
5134 <div id="vigilante-section-audit-recent" class="vigilante-settings-section">
5135 <h2><?php esc_html_e( 'Recent Activity', 'vigilante' ); ?></h2>
5136
5137 <?php
5138 $logs = $this->activity_log->get_logs( array( 'per_page' => 20 ) );
5139 $total_logs = $this->activity_log->get_logs_count();
5140
5141 // Label maps for translated display
5142 $type_labels = array(
5143 'login' => __( 'Login', 'vigilante' ),
5144 'user' => __( 'User', 'vigilante' ),
5145 'content' => __( 'Content', 'vigilante' ),
5146 'plugin' => __( 'Plugin', 'vigilante' ),
5147 'theme' => __( 'Theme', 'vigilante' ),
5148 'settings' => __( 'Settings', 'vigilante' ),
5149 'comment' => __( 'Comment', 'vigilante' ),
5150 'media' => __( 'Media', 'vigilante' ),
5151 'firewall' => __( 'Firewall', 'vigilante' ),
5152 'file' => __( 'File', 'vigilante' ),
5153 'security' => __( 'Security', 'vigilante' ),
5154 'system' => __( 'System', 'vigilante' ),
5155 );
5156 $severity_labels = array(
5157 'info' => __( 'Info', 'vigilante' ),
5158 'warning' => __( 'Warning', 'vigilante' ),
5159 'critical' => __( 'Critical', 'vigilante' ),
5160 );
5161
5162 $firewall_options = $this->settings->get_section( 'firewall' );
5163 $ip_whitelist = $firewall_options['ip_whitelist'] ?? array();
5164 $ip_blacklist = $firewall_options['ip_blacklist'] ?? array();
5165 $ua_whitelist = $firewall_options['ua_whitelist'] ?? array();
5166 $ua_blacklist = $firewall_options['ua_blacklist'] ?? array();
5167 ?>
5168
5169 <div class="vigilante-log-filters">
5170 <input type="text" id="vigilante-log-search" size="1" placeholder="<?php esc_attr_e( 'Search logs (min. 3 characters)...', 'vigilante' ); ?>" class="vigilante-log-search-input">
5171 <select id="vigilante-log-type-filter">
5172 <option value=""><?php esc_html_e( 'All Types', 'vigilante' ); ?></option>
5173 <option value="login"><?php esc_html_e( 'Login', 'vigilante' ); ?></option>
5174 <option value="user"><?php esc_html_e( 'User', 'vigilante' ); ?></option>
5175 <option value="content"><?php esc_html_e( 'Content', 'vigilante' ); ?></option>
5176 <option value="plugin"><?php esc_html_e( 'Plugin', 'vigilante' ); ?></option>
5177 <option value="theme"><?php esc_html_e( 'Theme', 'vigilante' ); ?></option>
5178 <option value="settings"><?php esc_html_e( 'Settings', 'vigilante' ); ?></option>
5179 <option value="comment"><?php esc_html_e( 'Comment', 'vigilante' ); ?></option>
5180 <option value="media"><?php esc_html_e( 'Media', 'vigilante' ); ?></option>
5181 <option value="firewall"><?php esc_html_e( 'Firewall', 'vigilante' ); ?></option>
5182 <option value="file"><?php esc_html_e( 'File', 'vigilante' ); ?></option>
5183 <option value="security"><?php esc_html_e( 'Security', 'vigilante' ); ?></option>
5184 <option value="system"><?php esc_html_e( 'System', 'vigilante' ); ?></option>
5185 </select>
5186 <select id="vigilante-log-severity-filter">
5187 <option value=""><?php esc_html_e( 'All Severities', 'vigilante' ); ?></option>
5188 <option value="info"><?php esc_html_e( 'Info', 'vigilante' ); ?></option>
5189 <option value="warning"><?php esc_html_e( 'Warning', 'vigilante' ); ?></option>
5190 <option value="critical"><?php esc_html_e( 'Critical', 'vigilante' ); ?></option>
5191 </select>
5192 <select id="vigilante-log-method-filter">
5193 <option value=""><?php esc_html_e( 'All Methods', 'vigilante' ); ?></option>
5194 <option value="GET">GET</option>
5195 <option value="POST">POST</option>
5196 <option value="PUT">PUT</option>
5197 <option value="DELETE">DELETE</option>
5198 <option value="PATCH">PATCH</option>
5199 <option value="OPTIONS">OPTIONS</option>
5200 <option value="HEAD">HEAD</option>
5201 </select>
5202 <button type="button" id="vigilante-log-refresh" class="button"><?php esc_html_e( 'Refresh', 'vigilante' ); ?></button>
5203 <span class="vigilante-pagination" id="vigilante-log-pagination" data-total="<?php echo esc_attr( $total_logs ); ?>" data-per-page="20" data-page="1">
5204 <?php if ( $total_logs > 20 ) : ?>
5205 <button type="button" class="vigilante-page-first" title="<?php esc_attr_e( 'First page', 'vigilante' ); ?>" disabled>&laquo;</button>
5206 <button type="button" class="vigilante-page-prev" title="<?php esc_attr_e( 'Previous page', 'vigilante' ); ?>" disabled>&lsaquo;</button>
5207 <?php endif; ?>
5208 <span class="vigilante-page-info">
5209 <?php
5210 $showing = min( 20, $total_logs );
5211 printf(
5212 /* translators: 1: first item, 2: last item, 3: total items */
5213 esc_html__( '%1$d–%2$d of %3$d', 'vigilante' ),
5214 $total_logs > 0 ? 1 : 0,
5215 absint( $showing ),
5216 absint( $total_logs )
5217 );
5218 ?>
5219 </span>
5220 <?php if ( $total_logs > 20 ) : ?>
5221 <button type="button" class="vigilante-page-next" title="<?php esc_attr_e( 'Next page', 'vigilante' ); ?>">&rsaquo;</button>
5222 <button type="button" class="vigilante-page-last" title="<?php esc_attr_e( 'Last page', 'vigilante' ); ?>">&raquo;</button>
5223 <?php endif; ?>
5224 </span>
5225 </div>
5226
5227 <div class="vigilante-log-table-wrap">
5228 <table id="vigilante-activity-log-table" class="wp-list-table widefat striped">
5229 <thead>
5230 <tr>
5231 <th class="column-date"><?php esc_html_e( 'Date', 'vigilante' ); ?></th>
5232 <th class="column-type"><?php esc_html_e( 'Type', 'vigilante' ); ?></th>
5233 <th class="column-method"><?php esc_html_e( 'Method', 'vigilante' ); ?></th>
5234 <th class="column-severity"><?php esc_html_e( 'Severity', 'vigilante' ); ?></th>
5235 <th class="column-message"><?php esc_html_e( 'Message', 'vigilante' ); ?></th>
5236 <th class="column-user"><?php esc_html_e( 'User', 'vigilante' ); ?></th>
5237 <th class="column-ip"><?php esc_html_e( 'IP', 'vigilante' ); ?></th>
5238 <th class="column-details"><?php esc_html_e( 'Details', 'vigilante' ); ?></th>
5239 </tr>
5240 </thead>
5241 <tbody>
5242 <?php
5243 if ( empty( $logs ) ) :
5244 ?>
5245 <tr><td colspan="8"><?php esc_html_e( 'No log entries found.', 'vigilante' ); ?></td></tr>
5246 <?php else : ?>
5247 <?php foreach ( $logs as $log ) :
5248 $request_method = isset( $log->request_method ) ? $log->request_method : '';
5249 // Prepare details as a simple object
5250 $ip_val = (string) ( $log->ip_address ?? '' );
5251 $ua_val = (string) ( $log->user_agent ?? '' );
5252 $details = array(
5253 'id' => (int) $log->id,
5254 'type' => (string) ( $log->event_type ?? '' ),
5255 'action' => (string) ( $log->event_action ?? '' ),
5256 'message' => (string) ( $log->event_message ?? '' ),
5257 'user' => (string) ( $log->user_login ?? '' ),
5258 'ip' => $ip_val,
5259 'user_agent' => $ua_val,
5260 'request_method' => (string) $request_method,
5261 'date' => (string) ( $log->created_at ?? '' ),
5262 'severity' => (string) ( $log->severity ?? 'info' ),
5263 'is_ip_whitelisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_whitelist, true ) ),
5264 'is_ip_blacklisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_blacklist, true ) ),
5265 'is_ua_whitelisted' => ( '' !== $ua_val && in_array( $ua_val, $ua_whitelist, true ) ),
5266 'is_ua_blacklisted' => ( '' !== $ua_val && in_array( $ua_val, $ua_blacklist, true ) ),
5267 );
5268 $display_type = isset( $type_labels[ $log->event_type ] ) ? $type_labels[ $log->event_type ] : $log->event_type;
5269 $display_severity = isset( $severity_labels[ $log->severity ] ) ? $severity_labels[ $log->severity ] : $log->severity;
5270 ?>
5271 <tr class="vigilante-severity-<?php echo esc_attr( $log->severity ); ?>">
5272 <td><?php echo esc_html( $log->created_at ); ?></td>
5273 <td><?php echo esc_html( $display_type ); ?></td>
5274 <td><?php if ( ! empty( $request_method ) ) : ?><span class="vigilante-method-label vigilante-method-<?php echo esc_attr( strtolower( $request_method ) ); ?>"><?php echo esc_html( $request_method ); ?></span><?php else : ?>-<?php endif; ?></td>
5275 <td><span class="vigilante-badge vigilante-badge-<?php echo esc_attr( $log->severity ); ?>"><?php echo esc_html( $display_severity ); ?></span></td>
5276 <td><?php echo esc_html( $log->event_message ); ?></td>
5277 <td><?php echo esc_html( $log->user_login ?? '-' ); ?></td>
5278 <td><code><?php echo esc_html( $log->ip_address ); ?></code></td>
5279 <td>
5280 <button type="button" class="button button-small vigilante-view-log-details"
5281 data-details='<?php echo esc_attr( wp_json_encode( $details, JSON_HEX_APOS | JSON_HEX_QUOT ) ); ?>'>
5282 <?php esc_html_e( 'View', 'vigilante' ); ?>
5283 </button>
5284 </td>
5285 </tr>
5286 <?php endforeach; ?>
5287 <?php endif; ?>
5288 </tbody>
5289 </table>
5290 </div>
5291
5292 <!-- Log Details Modal -->
5293 <div id="vigilante-log-details-modal" class="vigilante-modal" style="display: none;">
5294 <div class="vigilante-modal-content">
5295 <span class="vigilante-modal-close">&times;</span>
5296 <h3><?php esc_html_e( 'Log Entry Details', 'vigilante' ); ?></h3>
5297 <div id="vigilante-log-details-content"></div>
5298 </div>
5299 </div>
5300
5301 <p>
5302 <button type="button" class="button vigilante-export-logs"><?php esc_html_e( 'Export Audit Log', 'vigilante' ); ?></button>
5303 <button type="button" class="button vigilante-clear-logs" style="color: #a00;"><?php esc_html_e( 'Clear All Logs', 'vigilante' ); ?></button>
5304 </p>
5305 </div>
5306 <?php
5307 }
5308
5309 /**
5310 * Render File Integrity tab
5311 */
5312 private function render_tab_file_integrity() {
5313 $is_disabled = $this->render_module_disabled_notice( 'file_integrity' );
5314 $options = $this->settings->get_section( 'file_integrity' );
5315 $last_scan = get_option( 'vigilante_last_integrity_scan' );
5316 $last_results = get_option( 'vigilante_last_integrity_results' );
5317 $ignored_files = get_option( 'vigilante_ignored_files', array() );
5318
5319 // Backward compat: convert old notify_on_changes to notify_level
5320 $notify_level = $options['notify_level'] ?? '';
5321 if ( empty( $notify_level ) ) {
5322 $notify_level = ! empty( $options['notify_on_changes'] ) ? 'all' : 'disabled';
5323 }
5324
5325 // Closed + Removed plugins data. Surfaced inside Last Scan Results so the
5326 // user sees file findings and plugin closures together (same tier of risk,
5327 // same UI), and as the trigger to keep Last Scan Results open even when no
5328 // file scan has run yet (the daily cron may have populated this section).
5329 //
5330 // Gating by last_check_time > 0 is how "Clear Previous Results" visually
5331 // resets this block: the option vigilante_plugin_status_last_check is
5332 // deleted on Clear, but the state map and the ignored list survive so the
5333 // next scan reconstructs without degrading a 'removed' slug. While
5334 // last_check is 0, we treat the plugin_status data as if it didn't exist.
5335 if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) {
5336 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
5337 }
5338 $closed_checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
5339 $closed_last_check = $closed_checker->get_last_check_time();
5340 if ( $closed_last_check > 0 ) {
5341 $closed_plugins = $closed_checker->get_closed_plugins();
5342 $ignored_closed_plugins = $closed_checker->get_ignored_closed_plugins();
5343 } else {
5344 $closed_plugins = array();
5345 $ignored_closed_plugins = array();
5346 }
5347 $has_closed = ! empty( $closed_plugins );
5348 $datetime_format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
5349 ?>
5350 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="file_integrity" <?php echo $is_disabled ? 'inert' : ''; ?>>
5351 <div id="vigilante-section-fi-monitoring" class="vigilante-settings-section">
5352 <h2>
5353 <?php esc_html_e( 'File Integrity Monitoring', 'vigilante' ); ?>
5354 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5355 </h2>
5356 <p><?php esc_html_e( 'Detects file modifications using WordPress.org checksums.', 'vigilante' ); ?></p>
5357
5358 <table class="form-table">
5359 <tr>
5360 <th scope="row"><?php esc_html_e( 'Automatic Scans', 'vigilante' ); ?></th>
5361 <td>
5362 <label>
5363 <input type="checkbox" name="file_integrity[auto_scan]" value="1" <?php checked( ! empty( $options['auto_scan'] ) ); ?>>
5364 <?php esc_html_e( 'Enable scheduled file integrity scans', 'vigilante' ); ?>
5365 </label>
5366 </td>
5367 </tr>
5368 <tr>
5369 <th scope="row"><?php esc_html_e( 'Scan Frequency', 'vigilante' ); ?></th>
5370 <td>
5371 <select name="file_integrity[scan_frequency]">
5372 <option value="daily" <?php selected( $options['scan_frequency'] ?? 'daily', 'daily' ); ?>><?php esc_html_e( 'Daily', 'vigilante' ); ?></option>
5373 <option value="weekly" <?php selected( $options['scan_frequency'] ?? 'daily', 'weekly' ); ?>><?php esc_html_e( 'Weekly', 'vigilante' ); ?></option>
5374 </select>
5375 </td>
5376 </tr>
5377 <tr>
5378 <th scope="row"><?php esc_html_e( 'Email Notifications', 'vigilante' ); ?></th>
5379 <td>
5380 <select name="file_integrity[notify_level]">
5381 <option value="all" <?php selected( $notify_level, 'all' ); ?>><?php esc_html_e( 'All issues (modified + suspicious)', 'vigilante' ); ?></option>
5382 <option value="suspicious_only" <?php selected( $notify_level, 'suspicious_only' ); ?>><?php esc_html_e( 'Suspicious files only', 'vigilante' ); ?></option>
5383 <option value="disabled" <?php selected( $notify_level, 'disabled' ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option>
5384 </select>
5385 <p class="description"><?php esc_html_e( '"Suspicious files only" reduces noise by skipping modified file notifications. Recommended for most sites.', 'vigilante' ); ?></p>
5386 </td>
5387 </tr>
5388 <tr>
5389 <th scope="row"><?php esc_html_e( 'Instant Alert', 'vigilante' ); ?></th>
5390 <td>
5391 <label>
5392 <input type="checkbox" name="file_integrity[instant_alert]" value="1" <?php checked( ! empty( $options['instant_alert'] ) ); ?>>
5393 <?php esc_html_e( 'Send immediate alert when modified, suspicious or additional files are detected, or when a closed plugin is found', 'vigilante' ); ?>
5394 </label>
5395 <p class="description"><?php esc_html_e( 'Fires even if the Email Notifications setting above is set to Disabled.', 'vigilante' ); ?></p>
5396 <p class="description">
5397 <?php
5398 printf(
5399 /* translators: %s: Link to notification settings */
5400 esc_html__( '&#9432; Notifications are sent to the recipients configured in %s.', 'vigilante' ),
5401 '<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>'
5402 );
5403 ?>
5404 </p>
5405 </td>
5406 </tr>
5407 <tr>
5408 <th scope="row"><?php esc_html_e( 'Test email', 'vigilante' ); ?></th>
5409 <td>
5410 <button type="button" class="button vigilante-test-email-btn" data-original-text="<?php esc_attr_e( 'Send test email', 'vigilante' ); ?>">
5411 <?php esc_html_e( 'Send test email', 'vigilante' ); ?>
5412 </button>
5413 <span class="vigilante-test-email-result" style="margin-left:8px;"></span>
5414 <p class="description"><?php esc_html_e( 'Sends a test message to the configured recipients to confirm email delivery works.', 'vigilante' ); ?></p>
5415 </td>
5416 </tr>
5417 <tr>
5418 <th scope="row"><?php esc_html_e( 'Scan Scope', 'vigilante' ); ?></th>
5419 <td>
5420 <fieldset>
5421 <label>
5422 <input type="checkbox" name="file_integrity[scan_core]" value="1" <?php checked( $options['scan_core'] ?? true ); ?>>
5423 <?php esc_html_e( 'Core files (compare against WordPress.org checksums)', 'vigilante' ); ?>
5424 </label>
5425 <br>
5426 <label>
5427 <input type="checkbox" name="file_integrity[scan_plugins]" value="1" <?php checked( $options['scan_plugins'] ?? true ); ?>>
5428 <?php esc_html_e( 'Plugins (WordPress.org repository plugins)', 'vigilante' ); ?>
5429 </label>
5430 <br>
5431 <label>
5432 <input type="checkbox" name="file_integrity[scan_themes]" value="1" <?php checked( $options['scan_themes'] ?? true ); ?>>
5433 <?php esc_html_e( 'Themes (WordPress.org repository themes)', 'vigilante' ); ?>
5434 </label>
5435 <br>
5436 <label>
5437 <input type="checkbox" name="file_integrity[scan_uploads]" value="1" <?php checked( $options['scan_uploads'] ?? true ); ?>>
5438 <?php esc_html_e( 'Uploads directory (detect PHP files, double extensions, .htaccess)', 'vigilante' ); ?>
5439 </label>
5440 <br>
5441 <label>
5442 <input type="checkbox" name="file_integrity[scan_critical_config]" value="1" <?php checked( $options['scan_critical_config'] ?? true ); ?>>
5443 <?php esc_html_e( 'Critical config files (wp-config.php, .htaccess baseline monitoring)', 'vigilante' ); ?>
5444 </label>
5445 <br>
5446 <label>
5447 <input type="checkbox" name="file_integrity[check_closed_plugins]" value="1" <?php checked( $options['check_closed_plugins'] ?? true ); ?>>
5448 <?php esc_html_e( 'Closed plugins (daily check against the WordPress.org repository)', 'vigilante' ); ?>
5449 </label>
5450 </fieldset>
5451 </td>
5452 </tr>
5453 <tr>
5454 <th scope="row"><?php esc_html_e( 'Excluded Paths', 'vigilante' ); ?></th>
5455 <td>
5456 <textarea name="file_integrity[excluded_paths]" rows="4" class="large-text code" placeholder="wp-content/cache&#10;wp-content/languages"><?php echo esc_textarea( implode( "\n", $options['excluded_paths'] ?? array() ) ); ?></textarea>
5457 <p class="description"><?php esc_html_e( 'One path per line (relative to WordPress root). Files within these paths will be skipped during scans.', 'vigilante' ); ?></p>
5458 </td>
5459 </tr>
5460 <tr>
5461 <th scope="row"><?php esc_html_e( 'Excluded Extensions', 'vigilante' ); ?></th>
5462 <td>
5463 <textarea name="file_integrity[excluded_extensions]" rows="3" class="large-text code" placeholder=".log&#10;.po&#10;.mo&#10;.pot"><?php echo esc_textarea( implode( "\n", $options['excluded_extensions'] ?? array() ) ); ?></textarea>
5464 <p class="description"><?php esc_html_e( 'One extension per line (e.g. .log, .po, .mo). Files with these extensions will be skipped. Useful to avoid false positives from translation or log files.', 'vigilante' ); ?></p>
5465 </td>
5466 </tr>
5467 </table>
5468 </div>
5469
5470 <p class="submit vigilante-submit-buttons">
5471 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
5472 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
5473 </button>
5474 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
5475 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
5476 </button>
5477 <span class="vigilante-buttons-separator"></span>
5478 <button type="button" class="button button-primary vigilante-run-scan">
5479 <?php esc_html_e( 'Run Scan Now', 'vigilante' ); ?>
5480 </button>
5481 <button type="button" class="button vigilante-clear-scan-btn vigilante-clear-scan">
5482 <?php esc_html_e( 'Clear Previous Results', 'vigilante' ); ?>
5483 </button>
5484 </p>
5485 </form>
5486
5487 <div id="vigilante-scan-results" class="vigilante-settings-section" style="display:none;"></div>
5488
5489 <?php if ( $last_scan || $has_closed || $closed_last_check > 0 ) : ?>
5490 <div id="vigilante-section-fi-last-scan" class="vigilante-settings-section">
5491 <h2><?php esc_html_e( 'Last Scan Results', 'vigilante' ); ?></h2>
5492 <?php if ( $last_scan ) : ?>
5493 <p>
5494 <?php
5495 // Build the "X files scanned" hint inline with the date so it doesn't
5496 // need its own stat box (keeps the row compact when closed plugins are
5497 // present).
5498 $scanned_total = 0;
5499 if ( $last_results ) {
5500 $scanned_total = (int) ( $last_results['ok'] ?? 0 )
5501 + count( $last_results['modified'] ?? array() )
5502 + count( $last_results['suspicious'] ?? array() )
5503 + count( $last_results['extra'] ?? array() )
5504 + count( $ignored_files );
5505 }
5506 if ( $scanned_total > 0 ) {
5507 printf(
5508 /* translators: 1: date and time of last scan, 2: formatted file count */
5509 esc_html__( 'Last scan: %1$s (%2$s files scanned)', 'vigilante' ),
5510 esc_html( wp_date( $datetime_format, $last_scan ) ),
5511 esc_html( number_format_i18n( $scanned_total ) )
5512 );
5513 } else {
5514 printf(
5515 /* translators: %s: date and time of last scan */
5516 esc_html__( 'Last scan: %s', 'vigilante' ),
5517 esc_html( wp_date( $datetime_format, $last_scan ) )
5518 );
5519 }
5520 if ( $closed_last_check > 0 && $closed_last_check !== (int) $last_scan ) {
5521 echo ' &middot; ';
5522 printf(
5523 /* translators: %s: date and time of last closed plugins check */
5524 esc_html__( 'Closed plugins last checked: %s', 'vigilante' ),
5525 esc_html( wp_date( $datetime_format, $closed_last_check ) )
5526 );
5527 }
5528 ?>
5529 </p>
5530 <?php elseif ( $closed_last_check > 0 ) : ?>
5531 <p>
5532 <?php
5533 printf(
5534 /* translators: %s: date and time of last closed plugins check */
5535 esc_html__( 'Closed plugins last checked: %s &middot; the daily cron is running, no full integrity scan yet.', 'vigilante' ),
5536 esc_html( wp_date( $datetime_format, $closed_last_check ) )
5537 );
5538 ?>
5539 </p>
5540 <?php endif; ?>
5541 <div id="vigilante-last-scan-results">
5542 <?php if ( $last_results || $has_closed ) : ?>
5543 <div class="vigilante-scan-summary">
5544 <?php if ( $last_results ) : ?>
5545 <div class="vigilante-scan-stat vigilante-stat-ok">
5546 <span class="vigilante-stat-number"><?php echo esc_html( $last_results['ok'] ?? 0 ); ?></span>
5547 <span class="vigilante-stat-label"><?php esc_html_e( 'OK', 'vigilante' ); ?></span>
5548 </div>
5549 <div class="vigilante-scan-stat vigilante-stat-modified">
5550 <span class="vigilante-stat-number"><?php echo esc_html( count( $last_results['modified'] ?? array() ) ); ?></span>
5551 <span class="vigilante-stat-label"><?php esc_html_e( 'Modified', 'vigilante' ); ?></span>
5552 </div>
5553 <div class="vigilante-scan-stat vigilante-stat-suspicious">
5554 <span class="vigilante-stat-number"><?php echo esc_html( count( $last_results['suspicious'] ?? array() ) ); ?></span>
5555 <span class="vigilante-stat-label"><?php esc_html_e( 'Suspicious', 'vigilante' ); ?></span>
5556 </div>
5557 <div class="vigilante-scan-stat vigilante-stat-extra">
5558 <span class="vigilante-stat-number"><?php echo esc_html( count( $last_results['extra'] ?? array() ) ); ?></span>
5559 <span class="vigilante-stat-label"><?php esc_html_e( 'Extra', 'vigilante' ); ?></span>
5560 </div>
5561 <?php endif; ?>
5562 <?php if ( $has_closed ) : ?>
5563 <div class="vigilante-scan-stat vigilante-stat-suspicious">
5564 <span class="vigilante-stat-number" style="color: #d63638;"><?php echo (int) count( $closed_plugins ); ?></span>
5565 <span class="vigilante-stat-label"><?php esc_html_e( 'Closed/Removed', 'vigilante' ); ?></span>
5566 </div>
5567 <?php endif; ?>
5568 <?php if ( ! empty( $ignored_files ) ) : ?>
5569 <div class="vigilante-scan-stat vigilante-stat-ignored">
5570 <span class="vigilante-stat-number"><?php echo esc_html( count( $ignored_files ) ); ?></span>
5571 <span class="vigilante-stat-label"><?php esc_html_e( 'Ignored', 'vigilante' ); ?></span>
5572 </div>
5573 <?php endif; ?>
5574 </div>
5575
5576 <?php if ( ! empty( $last_results['suspicious'] ) ) : ?>
5577 <div class="vigilante-file-list vigilante-suspicious-files vigilante-paginated-section" data-bulk-mode="ignore">
5578 <h3 style="color: #d63638;"><?php esc_html_e( 'Suspicious Files', 'vigilante' ); ?></h3>
5579 <p class="description" style="color: #d63638;"><?php esc_html_e( '&#9888; Warning: These files may contain malicious code or are in unexpected locations. Review immediately!', 'vigilante' ); ?></p>
5580 <div class="vigilante-fi-bulk-bar">
5581 <button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button>
5582 <span class="vigilante-fi-bulk-count" aria-live="polite"></span>
5583 </div>
5584 <div class="vigilante-fi-pagination-wrap"></div>
5585 <table class="wp-list-table widefat fixed striped vigilante-fi-paginated">
5586 <thead>
5587 <tr>
5588 <td class="manage-column column-cb check-column"><input type="checkbox" class="vigilante-fi-cb-all" aria-label="<?php esc_attr_e( 'Select all', 'vigilante' ); ?>"></td>
5589 <th><?php esc_html_e( 'File', 'vigilante' ); ?></th>
5590 <th style="width: 250px;"><?php esc_html_e( 'Reason', 'vigilante' ); ?></th>
5591 <th style="width: 120px;"><?php esc_html_e( 'Type', 'vigilante' ); ?></th>
5592 <th style="width: 80px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
5593 </tr>
5594 </thead>
5595 <tbody>
5596 <?php
5597 foreach ( $last_results['suspicious'] as $item ) {
5598 $file_path = '';
5599 $file_reason = __( 'Unknown', 'vigilante' );
5600 $file_type = 'unknown';
5601
5602 if ( is_array( $item ) ) {
5603 if ( isset( $item['file'] ) ) {
5604 $file_path = $item['file'];
5605 }
5606 if ( isset( $item['reason'] ) ) {
5607 $file_reason = $item['reason'];
5608 }
5609 if ( isset( $item['type'] ) ) {
5610 $file_type = $item['type'];
5611 }
5612 } else {
5613 $file_path = (string) $item;
5614 }
5615 ?>
5616 <tr>
5617 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th>
5618 <td><code style="color: #d63638;"><?php echo esc_html( $file_path ); ?></code></td>
5619 <td><?php echo esc_html( $file_reason ); ?></td>
5620 <td><?php echo esc_html( $file_type ); ?></td>
5621 <td><button type="button" class="button button-small vigilante-ignore-file" data-file="<?php echo esc_attr( $file_path ); ?>"><?php esc_html_e( 'Ignore', 'vigilante' ); ?></button></td>
5622 </tr>
5623 <?php
5624 }
5625 ?>
5626 </tbody>
5627 </table>
5628 </div>
5629 <?php endif; ?>
5630
5631 <?php if ( ! empty( $last_results['extra'] ) ) : ?>
5632 <div class="vigilante-file-list vigilante-extra-files vigilante-paginated-section" data-bulk-mode="ignore">
5633 <h3 style="color: #b32d2e;"><?php esc_html_e( 'Extra Files', 'vigilante' ); ?></h3>
5634 <p class="description"><?php esc_html_e( 'PHP files found in plugins or themes that are not part of the original distribution from WordPress.org. May be legitimate customizations or injected backdoors.', 'vigilante' ); ?></p>
5635 <div class="vigilante-fi-bulk-bar">
5636 <button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button>
5637 <span class="vigilante-fi-bulk-count" aria-live="polite"></span>
5638 </div>
5639 <div class="vigilante-fi-pagination-wrap"></div>
5640 <table class="wp-list-table widefat fixed striped vigilante-fi-paginated">
5641 <thead>
5642 <tr>
5643 <td class="manage-column column-cb check-column"><input type="checkbox" class="vigilante-fi-cb-all" aria-label="<?php esc_attr_e( 'Select all', 'vigilante' ); ?>"></td>
5644 <th><?php esc_html_e( 'File', 'vigilante' ); ?></th>
5645 <th style="width: 250px;"><?php esc_html_e( 'Reason', 'vigilante' ); ?></th>
5646 <th style="width: 120px;"><?php esc_html_e( 'Type', 'vigilante' ); ?></th>
5647 <th style="width: 80px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
5648 </tr>
5649 </thead>
5650 <tbody>
5651 <?php
5652 foreach ( $last_results['extra'] as $item ) {
5653 $file_path = is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item;
5654 $file_reason = is_array( $item ) ? ( $item['reason'] ?? __( 'Unknown', 'vigilante' ) ) : __( 'Unknown', 'vigilante' );
5655 $file_type = is_array( $item ) ? ( $item['type'] ?? 'unknown' ) : 'unknown';
5656 ?>
5657 <tr>
5658 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th>
5659 <td><code style="color: #b32d2e;"><?php echo esc_html( $file_path ); ?></code></td>
5660 <td><?php echo esc_html( $file_reason ); ?></td>
5661 <td><?php echo esc_html( $file_type ); ?></td>
5662 <td><button type="button" class="button button-small vigilante-ignore-file" data-file="<?php echo esc_attr( $file_path ); ?>"><?php esc_html_e( 'Ignore', 'vigilante' ); ?></button></td>
5663 </tr>
5664 <?php
5665 }
5666 ?>
5667 </tbody>
5668 </table>
5669 </div>
5670 <?php endif; ?>
5671
5672 <?php
5673 // Split critical config files from regular modified files.
5674 // Computed unconditionally so the three sub-sections that consume
5675 // these arrays (Critical Config, Closed + Removed, Modified Files)
5676 // can render independently and in the order the team picked.
5677 $critical_modified = array();
5678 $regular_modified = array();
5679 if ( $last_results && ! empty( $last_results['modified'] ) ) {
5680 foreach ( $last_results['modified'] as $item ) {
5681 if ( is_array( $item ) && isset( $item['type'] ) && 'critical_config' === $item['type'] ) {
5682 $critical_modified[] = $item;
5683 } else {
5684 $regular_modified[] = $item;
5685 }
5686 }
5687 }
5688 ?>
5689
5690 <?php if ( ! empty( $critical_modified ) ) : ?>
5691 <div class="vigilante-file-list vigilante-critical-config-files">
5692 <h3 style="color: #e36210;"><?php esc_html_e( 'Critical config files modified', 'vigilante' ); ?></h3>
5693 <p class="description">
5694 <?php esc_html_e( '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' ); ?>
5695 </p>
5696 <table class="wp-list-table widefat fixed striped">
5697 <thead>
5698 <tr>
5699 <th><?php esc_html_e( 'File', 'vigilante' ); ?></th>
5700 <th style="width: 200px;"><?php esc_html_e( 'Changes', 'vigilante' ); ?></th>
5701 <th style="width: 220px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
5702 </tr>
5703 </thead>
5704 <tbody>
5705 <?php foreach ( $critical_modified as $crit_item ) :
5706 $crit_file = $crit_item['file'] ?? '';
5707 $crit_baseline_size = $crit_item['baseline_size'] ?? 0;
5708 $crit_current_size = $crit_item['current_size'] ?? 0;
5709 $crit_diff = $crit_item['diff'] ?? array();
5710 $crit_id = sanitize_html_class( $crit_file );
5711 $added_count = is_array( $crit_diff ) ? count( $crit_diff['added'] ?? array() ) : 0;
5712 $removed_count = is_array( $crit_diff ) ? count( $crit_diff['removed'] ?? array() ) : 0;
5713 $diff_unavailable = is_array( $crit_diff ) && ! empty( $crit_diff['unavailable'] );
5714 ?>
5715 <tr>
5716 <td><code style="color: #e36210;"><?php echo esc_html( $crit_file ); ?></code></td>
5717 <td>
5718 <?php if ( ! $diff_unavailable ) : ?>
5719 <span style="color: #007017;">+<?php echo (int) $added_count; ?></span>
5720 <span style="color: #b32d2e;">-<?php echo (int) $removed_count; ?></span>
5721 <?php esc_html_e( 'lines', 'vigilante' ); ?><br>
5722 <?php endif; ?>
5723 <small style="color: #50575e;">
5724 <?php
5725 printf(
5726 /* translators: 1: baseline size, 2: current size */
5727 esc_html__( '%1$s &rarr; %2$s bytes', 'vigilante' ),
5728 esc_html( number_format_i18n( $crit_baseline_size ) ),
5729 esc_html( number_format_i18n( $crit_current_size ) )
5730 );
5731 ?>
5732 </small>
5733 </td>
5734 <td>
5735 <button type="button" class="button button-small vigilante-toggle-critical-content" data-target="vigilante-critical-content-<?php echo esc_attr( $crit_id ); ?>" data-label-show="<?php esc_attr_e( 'Review changes', 'vigilante' ); ?>" data-label-hide="<?php esc_attr_e( 'Hide changes', 'vigilante' ); ?>">
5736 <?php esc_html_e( 'Review changes', 'vigilante' ); ?>
5737 </button>
5738 <button type="button" class="button button-small button-primary vigilante-approve-critical-file" data-file="<?php echo esc_attr( $crit_file ); ?>">
5739 <?php esc_html_e( 'Approve', 'vigilante' ); ?>
5740 </button>
5741 </td>
5742 </tr>
5743 <tr id="vigilante-critical-content-<?php echo esc_attr( $crit_id ); ?>" class="vigilante-critical-content-row" style="display:none;">
5744 <td colspan="3" style="padding: 0;">
5745 <div class="vigilante-critical-content" style="max-height: 400px; overflow: auto; background: #fff; padding: 10px; font-size: 12px; line-height: 1.5; font-family: Consolas, Monaco, monospace; border-top: 1px solid #c3c4c7;">
5746 <?php if ( $diff_unavailable ) : ?>
5747 <p style="color: #50575e; font-style: italic; margin: 0;">
5748 <?php esc_html_e( 'Diff not available for this file (baseline was created before diff tracking was added). Approve to enable diff on future changes.', 'vigilante' ); ?>
5749 </p>
5750 <?php elseif ( empty( $crit_diff['added'] ) && empty( $crit_diff['removed'] ) ) : ?>
5751 <p style="color: #50575e; font-style: italic; margin: 0;">
5752 <?php esc_html_e( 'No line-level changes detected (may be whitespace or reordering).', 'vigilante' ); ?>
5753 </p>
5754 <?php else : ?>
5755 <?php if ( ! empty( $crit_diff['removed'] ) ) : ?>
5756 <?php foreach ( $crit_diff['removed'] as $rline ) : ?>
5757 <div style="background: #fbeaea; color: #b32d2e; padding: 1px 4px; white-space: pre-wrap; word-wrap: break-word;"><span style="display: inline-block; width: 50px; color: #999; user-select: none;"><?php echo (int) $rline['line']; ?></span>- <?php echo esc_html( $rline['content'] ); ?></div>
5758 <?php endforeach; ?>
5759 <?php endif; ?>
5760 <?php if ( ! empty( $crit_diff['added'] ) ) : ?>
5761 <?php foreach ( $crit_diff['added'] as $aline ) : ?>
5762 <div style="background: #e6f4e9; color: #007017; padding: 1px 4px; white-space: pre-wrap; word-wrap: break-word;"><span style="display: inline-block; width: 50px; color: #999; user-select: none;"><?php echo (int) $aline['line']; ?></span>+ <?php echo esc_html( $aline['content'] ); ?></div>
5763 <?php endforeach; ?>
5764 <?php endif; ?>
5765 <?php endif; ?>
5766 </div>
5767 </td>
5768 </tr>
5769 <?php endforeach; ?>
5770 </tbody>
5771 </table>
5772 </div>
5773 <?php endif; ?>
5774
5775 <?php if ( $has_closed ) : ?>
5776 <div class="vigilante-file-list vigilante-closed-plugins">
5777 <h3 style="color: #d63638;"><?php esc_html_e( 'Closed + Removed Plugins', 'vigilante' ); ?></h3>
5778 <p class="description" style="color: #d63638;">
5779 <?php esc_html_e( '&#9888; Warning: These plugins have been closed in the WordPress.org repository. Closures usually indicate malware, security issues, guideline violations, or supply chain attacks. Uninstall and replace as soon as possible.', 'vigilante' ); ?>
5780 </p>
5781 <table class="wp-list-table widefat striped">
5782 <thead>
5783 <tr>
5784 <th><?php esc_html_e( 'Plugin', 'vigilante' ); ?></th>
5785 <th style="width: 70px;"><?php esc_html_e( 'Version', 'vigilante' ); ?></th>
5786 <th style="width: 90px;"><?php esc_html_e( 'State', 'vigilante' ); ?></th>
5787 <th style="width: 110px;"><?php esc_html_e( 'Closed date', 'vigilante' ); ?></th>
5788 <th><?php esc_html_e( 'Reason', 'vigilante' ); ?></th>
5789 <th style="width: 130px;"><?php esc_html_e( 'Detected', 'vigilante' ); ?></th>
5790 <th style="width: 90px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
5791 </tr>
5792 </thead>
5793 <tbody>
5794 <?php foreach ( $closed_plugins as $cp_slug => $cp_entry ) :
5795 $cp_state = $cp_entry['state'] ?? '';
5796 $cp_state_label = 'closed' === $cp_state ? __( 'Closed', 'vigilante' ) : __( 'Removed', 'vigilante' );
5797 $cp_state_color = 'closed' === $cp_state ? '#d63638' : '#b32d2e';
5798 $cp_reason = '';
5799 if ( ! empty( $cp_entry['closed_reason_text'] ) ) {
5800 $cp_reason = $cp_entry['closed_reason_text'];
5801 } elseif ( 'removed' === $cp_state ) {
5802 $cp_reason = __( 'Removed from repository (metadata hidden, typical of Security Issue closures)', 'vigilante' );
5803 }
5804 $cp_detected = isset( $cp_entry['first_detected'] ) ? (int) $cp_entry['first_detected'] : 0;
5805 ?>
5806 <tr>
5807 <td>
5808 <strong><?php echo esc_html( $cp_entry['name'] ?? $cp_slug ); ?></strong><br>
5809 <a href="<?php echo esc_url( 'https://wordpress.org/plugins/' . $cp_slug . '/' ); ?>" target="_blank" rel="noopener noreferrer"><code style="color: #50575e;"><?php echo esc_html( $cp_slug ); ?></code></a>
5810 </td>
5811 <td><?php echo esc_html( $cp_entry['version'] ?? '' ); ?></td>
5812 <td><span style="color: <?php echo esc_attr( $cp_state_color ); ?>; font-weight: 600;"><?php echo esc_html( $cp_state_label ); ?></span></td>
5813 <td><?php echo esc_html( $cp_entry['closed_date'] ?? '' ); ?></td>
5814 <td><?php echo esc_html( $cp_reason ); ?></td>
5815 <td>
5816 <?php echo $cp_detected > 0 ? esc_html( wp_date( $datetime_format, $cp_detected ) ) : '&mdash;'; ?>
5817 </td>
5818 <td>
5819 <button type="button" class="button button-small vigilante-ignore-closed-plugin" data-slug="<?php echo esc_attr( $cp_slug ); ?>">
5820 <?php esc_html_e( 'Ignore', 'vigilante' ); ?>
5821 </button>
5822 </td>
5823 </tr>
5824 <?php endforeach; ?>
5825 </tbody>
5826 </table>
5827 </div>
5828 <?php endif; ?>
5829
5830 <?php if ( ! empty( $regular_modified ) ) : ?>
5831 <div class="vigilante-file-list vigilante-paginated-section" data-bulk-mode="ignore">
5832 <h3><?php esc_html_e( 'Modified Files', 'vigilante' ); ?></h3>
5833 <p class="description"><?php esc_html_e( 'These files (apparently) differ from the original WordPress or plugin versions.', 'vigilante' ); ?></p>
5834 <div class="vigilante-fi-bulk-bar">
5835 <button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button>
5836 <span class="vigilante-fi-bulk-count" aria-live="polite"></span>
5837 </div>
5838 <div class="vigilante-fi-pagination-wrap"></div>
5839 <table class="wp-list-table widefat fixed striped vigilante-fi-paginated">
5840 <thead>
5841 <tr>
5842 <td class="manage-column column-cb check-column"><input type="checkbox" class="vigilante-fi-cb-all" aria-label="<?php esc_attr_e( 'Select all', 'vigilante' ); ?>"></td>
5843 <th><?php esc_html_e( 'File', 'vigilante' ); ?></th>
5844 <th style="width: 100px;"><?php esc_html_e( 'Type', 'vigilante' ); ?></th>
5845 <th style="width: 80px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
5846 </tr>
5847 </thead>
5848 <tbody>
5849 <?php
5850 foreach ( $regular_modified as $item ) {
5851 $file_path = '';
5852 $file_type = 'unknown';
5853
5854 if ( is_array( $item ) ) {
5855 if ( isset( $item['file'] ) ) {
5856 $file_path = $item['file'];
5857 }
5858 if ( isset( $item['type'] ) ) {
5859 $file_type = $item['type'];
5860 }
5861 } else {
5862 $file_path = (string) $item;
5863 }
5864 ?>
5865 <tr>
5866 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th>
5867 <td><code><?php echo esc_html( $file_path ); ?></code></td>
5868 <td><?php echo esc_html( $file_type ); ?></td>
5869 <td><button type="button" class="button button-small vigilante-ignore-file" data-file="<?php echo esc_attr( $file_path ); ?>"><?php esc_html_e( 'Ignore', 'vigilante' ); ?></button></td>
5870 </tr>
5871 <?php
5872 }
5873 ?>
5874 </tbody>
5875 </table>
5876 </div>
5877 <?php endif; ?>
5878
5879 <?php if ( $last_results && empty( $last_results['modified'] ) && empty( $last_results['suspicious'] ) && empty( $last_results['extra'] ) && ! $has_closed ) : ?>
5880 <p class="vigilante-all-clear" style="color: #00a32a; font-weight: bold;">
5881 <?php esc_html_e( 'Good Job! All files passed integrity check. No issues found.', 'vigilante' ); ?>
5882 </p>
5883 <?php endif; ?>
5884 <?php endif; ?>
5885 </div>
5886 </div>
5887 <?php endif; ?>
5888
5889 <?php if ( ! empty( $ignored_closed_plugins ) ) : ?>
5890 <div id="vigilante-section-fi-ignored-closed" class="vigilante-settings-section">
5891 <h2><?php esc_html_e( 'Ignored Closed + Removed Plugins', 'vigilante' ); ?></h2>
5892 <p class="description"><?php esc_html_e( 'These plugins remain closed/removed in WordPress.org but you have chosen to hide them from the main list and from email alerts. They are still installed on the site and still running their code &mdash; the silencing is purely cosmetic.', 'vigilante' ); ?></p>
5893 <table class="wp-list-table widefat fixed striped">
5894 <thead>
5895 <tr>
5896 <th><?php esc_html_e( 'Plugin', 'vigilante' ); ?></th>
5897 <th style="width: 110px;"><?php esc_html_e( 'State', 'vigilante' ); ?></th>
5898 <th style="width: 120px;"><?php esc_html_e( 'Closed date', 'vigilante' ); ?></th>
5899 <th style="width: 130px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
5900 </tr>
5901 </thead>
5902 <tbody>
5903 <?php foreach ( $ignored_closed_plugins as $icp_slug => $icp_entry ) :
5904 $icp_state = $icp_entry['state'] ?? '';
5905 $icp_state_label = 'closed' === $icp_state ? __( 'Closed', 'vigilante' ) : __( 'Removed', 'vigilante' );
5906 ?>
5907 <tr>
5908 <td>
5909 <strong><?php echo esc_html( $icp_entry['name'] ?? $icp_slug ); ?></strong><br>
5910 <a href="<?php echo esc_url( 'https://wordpress.org/plugins/' . $icp_slug . '/' ); ?>" target="_blank" rel="noopener noreferrer"><code style="color: #50575e;"><?php echo esc_html( $icp_slug ); ?></code></a>
5911 </td>
5912 <td><?php echo esc_html( $icp_state_label ); ?></td>
5913 <td><?php echo esc_html( $icp_entry['closed_date'] ?? '' ); ?></td>
5914 <td>
5915 <button type="button" class="button button-small vigilante-unignore-closed-plugin" data-slug="<?php echo esc_attr( $icp_slug ); ?>">
5916 <?php esc_html_e( 'Stop ignoring', 'vigilante' ); ?>
5917 </button>
5918 </td>
5919 </tr>
5920 <?php endforeach; ?>
5921 </tbody>
5922 </table>
5923 <p style="margin-top: 10px;">
5924 <button type="button" class="button vigilante-clear-ignored-closed-plugins"><?php esc_html_e( 'Clear All Ignored Closed + Removed Plugins', 'vigilante' ); ?></button>
5925 </p>
5926 </div>
5927 <?php endif; ?>
5928
5929 <?php if ( ! empty( $ignored_files ) ) : ?>
5930 <div id="vigilante-section-fi-ignored" class="vigilante-settings-section">
5931 <h2><?php esc_html_e( 'Ignored Files', 'vigilante' ); ?></h2>
5932 <p class="description"><?php esc_html_e( 'These files are excluded from scan results and email notifications. They will still be scanned but any findings will be hidden.', 'vigilante' ); ?></p>
5933 <div class="vigilante-paginated-section" data-bulk-mode="unignore">
5934 <div class="vigilante-fi-bulk-bar">
5935 <button type="button" class="button vigilante-bulk-unignore" disabled><?php esc_html_e( 'Stop ignoring selected', 'vigilante' ); ?></button>
5936 <span class="vigilante-fi-bulk-count" aria-live="polite"></span>
5937 </div>
5938 <div class="vigilante-fi-pagination-wrap"></div>
5939 <table class="wp-list-table widefat fixed striped vigilante-fi-paginated">
5940 <thead>
5941 <tr>
5942 <td class="manage-column column-cb check-column"><input type="checkbox" class="vigilante-fi-cb-all" aria-label="<?php esc_attr_e( 'Select all', 'vigilante' ); ?>"></td>
5943 <th><?php esc_html_e( 'File', 'vigilante' ); ?></th>
5944 <th style="width: 120px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
5945 </tr>
5946 </thead>
5947 <tbody>
5948 <?php foreach ( $ignored_files as $file ) : ?>
5949 <tr>
5950 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file ); ?>"></th>
5951 <td><code><?php echo esc_html( $file ); ?></code></td>
5952 <td><button type="button" class="button button-small vigilante-unignore-file" data-file="<?php echo esc_attr( $file ); ?>"><?php esc_html_e( 'Stop ignoring', 'vigilante' ); ?></button></td>
5953 </tr>
5954 <?php endforeach; ?>
5955 </tbody>
5956 </table>
5957 </div>
5958 <p style="margin-top: 10px;">
5959 <button type="button" class="button vigilante-clear-ignored"><?php esc_html_e( 'Clear All Ignored Files', 'vigilante' ); ?></button>
5960 </p>
5961 </div>
5962 <?php endif; ?>
5963 <?php
5964 }
5965
5966 /**
5967 * Render sidebar with promotional widgets
5968 */
5969 private function render_sidebar() {
5970 $promo_banner = new Vigilante_Promo_Banner( 'vigilante' );
5971 $promo_banner->render();
5972 }
5973
5974 /**
5975 * AJAX: Download a ZIP backup of the critical config files.
5976 *
5977 * Streams wp-config.php and .htaccess (and robots.txt if present) as a
5978 * downloadable archive. Config backups are no longer left as files under the
5979 * web root, so this hands the admin the archive directly.
5980 */
5981 public function ajax_download_files_backup() {
5982 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
5983
5984 if ( ! current_user_can( 'manage_options' ) ) {
5985 wp_die( esc_html__( 'Permission denied.', 'vigilante' ), 403 );
5986 }
5987
5988 $backup_manager = new Vigilante_Backup_Manager();
5989 $result = $backup_manager->stream_files_zip();
5990
5991 // stream_files_zip() exits on success; only a WP_Error returns here.
5992 if ( is_wp_error( $result ) ) {
5993 wp_die( esc_html( $result->get_error_message() ), 500 );
5994 }
5995 }
5996
5997 /**
5998 * AJAX: Save settings
5999 */
6000 public function ajax_save_settings() {
6001 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6002
6003 if ( ! current_user_can( 'manage_options' ) ) {
6004 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6005 }
6006
6007 $section = isset( $_POST['section'] ) ? sanitize_key( $_POST['section'] ) : '';
6008
6009 // Handle $_POST['data'] based on type
6010 if ( isset( $_POST['data'] ) && is_array( $_POST['data'] ) ) {
6011 $data = map_deep( wp_unslash( $_POST['data'] ), 'sanitize_text_field' );
6012 } elseif ( isset( $_POST['data'] ) ) {
6013 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
6014 $raw_data = wp_unslash( $_POST['data'] );
6015 parse_str( $raw_data, $data );
6016 $data = map_deep( $data, 'sanitize_textarea_field' );
6017 } else {
6018 $data = array();
6019 }
6020
6021 if ( empty( $section ) ) {
6022 wp_send_json_error( __( 'Invalid section.', 'vigilante' ) );
6023 }
6024
6025 // Check if 2FA is being enabled or method changed (for notification sending)
6026 $send_2fa_notification = false;
6027 $send_login_url_notification = false;
6028
6029 if ( 'login_security' === $section ) {
6030 // Read old state from DB
6031 $db_options = get_option( Vigilante_Settings::OPTION_NAME, array() );
6032 $old_2fa_enabled = ! empty( $db_options['login_security']['two_factor']['enabled'] );
6033 $old_method = $db_options['login_security']['two_factor']['method'] ?? 'email';
6034
6035 // Check new state from submitted data
6036 $new_2fa_enabled = false;
6037 $notify_on_enable = false;
6038 $new_method = isset( $data['login_security']['two_factor']['method'] )
6039 ? sanitize_key( $data['login_security']['two_factor']['method'] )
6040 : 'email';
6041
6042 if ( isset( $data['login_security']['two_factor']['enabled'] ) ) {
6043 $new_2fa_enabled = filter_var( $data['login_security']['two_factor']['enabled'], FILTER_VALIDATE_BOOLEAN );
6044 }
6045 if ( isset( $data['login_security']['two_factor']['notify_on_enable'] ) ) {
6046 $notify_on_enable = filter_var( $data['login_security']['two_factor']['notify_on_enable'], FILTER_VALIDATE_BOOLEAN );
6047 }
6048
6049 // Send notification if:
6050 // 1. 2FA is being enabled (was off, now on) OR
6051 // 2. Method changed while 2FA is enabled
6052 if ( $notify_on_enable && $new_2fa_enabled ) {
6053 if ( ! $old_2fa_enabled || ( $old_2fa_enabled && $old_method !== $new_method ) ) {
6054 $send_2fa_notification = true;
6055 }
6056 }
6057
6058 // Check if login URL changed and notification is enabled
6059 $old_login_url = $db_options['login_security']['custom_login_url'] ?? '';
6060 $new_login_url = isset( $data['login_security']['custom_login_url'] )
6061 ? sanitize_title( $data['login_security']['custom_login_url'] )
6062 : '';
6063 $notify_on_url_change = isset( $data['login_security']['notify_on_login_url_change'] )
6064 ? filter_var( $data['login_security']['notify_on_login_url_change'], FILTER_VALIDATE_BOOLEAN )
6065 : false;
6066
6067 if ( $notify_on_url_change && ! empty( $new_login_url ) && $new_login_url !== $old_login_url ) {
6068 $send_login_url_notification = true;
6069 }
6070 }
6071
6072 // Get defaults
6073 $defaults = $this->settings->get_default_options();
6074
6075 // Read ONLY saved options from database (not merged with defaults)
6076 $saved_options = get_option( Vigilante_Settings::OPTION_NAME, array() );
6077
6078 // Handle modules
6079 if ( 'modules' === $section && isset( $data['modules'] ) ) {
6080 if ( ! isset( $saved_options['modules'] ) ) {
6081 $saved_options['modules'] = array();
6082 }
6083 foreach ( $data['modules'] as $module => $enabled ) {
6084 $module = sanitize_key( $module );
6085 $saved_options['modules'][ $module ] = in_array( $enabled, array( '1', 1, 'true', true ), true );
6086 }
6087 // Clear active preset when modules change
6088 update_option( 'vigilante_active_preset', '' );
6089 } else {
6090 // Process primary section
6091 if ( isset( $data[ $section ] ) && is_array( $data[ $section ] ) ) {
6092 $section_defaults = isset( $defaults[ $section ] ) ? $defaults[ $section ] : array();
6093 $current_section = isset( $saved_options[ $section ] ) ? $saved_options[ $section ] : array();
6094
6095 // Process the submitted data
6096 $processed = $this->process_section_data( $data[ $section ], $section_defaults, $current_section );
6097
6098 // Save the processed section
6099 $saved_options[ $section ] = $processed;
6100
6101 // Clear active preset when any section settings change
6102 update_option( 'vigilante_active_preset', '' );
6103 }
6104 }
6105
6106 // Clear cache before saving
6107 wp_cache_delete( Vigilante_Settings::OPTION_NAME, 'options' );
6108
6109 // Save to database
6110 update_option( Vigilante_Settings::OPTION_NAME, $saved_options );
6111
6112 // Clear the settings cache
6113 $this->settings->clear_cache();
6114
6115 // Apply changes based on section
6116 $this->apply_section_changes( $section, $saved_options );
6117
6118 // Send 2FA notifications after settings are saved
6119 $notification_result = null;
6120 if ( $send_2fa_notification ) {
6121 $notification_result = $this->send_2fa_enable_notifications();
6122 }
6123
6124 // Send login URL notifications after settings are saved
6125 $login_url_result = null;
6126 if ( $send_login_url_notification ) {
6127 $login_url_result = $this->send_login_url_notifications();
6128 }
6129
6130 // Build success message
6131 $message = __( 'Settings saved successfully.', 'vigilante' );
6132
6133 if ( $notification_result && $notification_result['sent'] > 0 ) {
6134 $message .= ' ' . sprintf(
6135 /* translators: %d: Number of emails sent */
6136 _n(
6137 '2FA notification sent to %d user.',
6138 '2FA notifications sent to %d users.',
6139 $notification_result['sent'],
6140 'vigilante'
6141 ),
6142 $notification_result['sent']
6143 );
6144 }
6145
6146 if ( $login_url_result && $login_url_result['sent'] > 0 ) {
6147 $message .= ' ' . sprintf(
6148 /* translators: %d: Number of emails sent */
6149 _n(
6150 'Login URL notification sent to %d user.',
6151 'Login URL notifications sent to %d users.',
6152 $login_url_result['sent'],
6153 'vigilante'
6154 ),
6155 $login_url_result['sent']
6156 );
6157 }
6158
6159 wp_send_json_success( $message );
6160 }
6161
6162 /**
6163 * Send 2FA enable notifications to users
6164 *
6165 * @return array Result with 'sent' and 'failed' counts.
6166 */
6167 private function send_2fa_enable_notifications() {
6168 $result = array(
6169 'sent' => 0,
6170 'failed' => 0,
6171 );
6172
6173 // Get settings for roles and method
6174 $login_security = $this->settings->get_section( 'login_security' );
6175 $two_factor = isset( $login_security['two_factor'] ) ? $login_security['two_factor'] : array();
6176 $roles = isset( $two_factor['enforced_roles'] ) ? $two_factor['enforced_roles'] : array( 'administrator' );
6177 $method = isset( $two_factor['method'] ) ? $two_factor['method'] : 'email';
6178
6179 if ( empty( $roles ) ) {
6180 $roles = array( 'administrator' );
6181 }
6182
6183 $excluded = isset( $two_factor['excluded_users'] ) ? array_map( 'absint', $two_factor['excluded_users'] ) : array();
6184
6185 // Get users with these roles
6186 $args = array(
6187 'role__in' => $roles,
6188 );
6189 if ( ! empty( $excluded ) ) {
6190 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Small excluded users list from settings.
6191 $args['exclude'] = $excluded;
6192 }
6193 $users = get_users( $args );
6194
6195 if ( empty( $users ) ) {
6196 return $result;
6197 }
6198
6199 $site_name = get_bloginfo( 'name' );
6200 $from_name = ! empty( $two_factor['email_from_name'] ) ? $two_factor['email_from_name'] : $site_name;
6201
6202 if ( 'totp' === $method ) {
6203 // Use TOTP class for styled HTML emails
6204 if ( ! class_exists( 'Vigilante_Two_Factor_TOTP' ) ) {
6205 require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-totp.php';
6206 }
6207 $totp = new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log );
6208
6209 foreach ( $users as $user ) {
6210 // Skip users who already have TOTP configured
6211 $totp_data = $this->database->get_totp_data( $user->ID );
6212 if ( $totp_data && ! empty( $totp_data['is_configured'] ) ) {
6213 continue;
6214 }
6215
6216 $sent = $totp->send_activation_email( $user, $site_name, $from_name );
6217
6218 if ( $sent ) {
6219 $this->database->mark_2fa_notified( $user->ID );
6220 $result['sent']++;
6221 } else {
6222 $result['failed']++;
6223 }
6224 }
6225 } else {
6226 // Email method - use existing email 2FA class
6227 if ( ! class_exists( 'Vigilante_Two_Factor_Email' ) ) {
6228 require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-email.php';
6229 }
6230 $email_2fa = new Vigilante_Two_Factor_Email( $this->settings, $this->database, $this->activity_log );
6231 return $email_2fa->send_activation_notifications( false );
6232 }
6233
6234 return $result;
6235 }
6236
6237 /**
6238 * Send login URL change notifications to users with admin access
6239 *
6240 * @return array Result with 'sent' and 'failed' counts.
6241 */
6242 private function send_login_url_notifications() {
6243 $login_options = $this->settings->get_section( 'login_security' );
6244 $custom_url = ! empty( $login_options['custom_login_url'] ) ? sanitize_title( $login_options['custom_login_url'] ) : '';
6245
6246 if ( empty( $custom_url ) ) {
6247 return array( 'sent' => 0, 'failed' => 0 );
6248 }
6249
6250 $login_url = home_url( $custom_url . '/' );
6251 $site_name = get_bloginfo( 'name' );
6252
6253 $admin_roles = array( 'administrator', 'editor', 'author', 'contributor' );
6254 $users = get_users( array( 'role__in' => $admin_roles ) );
6255
6256 if ( empty( $users ) ) {
6257 return array( 'sent' => 0, 'failed' => 0 );
6258 }
6259
6260 $subject = sprintf(
6261 /* translators: %s: Site name */
6262 __( '[%s] Your login URL has changed', 'vigilante' ),
6263 $site_name
6264 );
6265
6266 $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' ) );
6267 $body .= Vigilante_Email_Template::url_box( $login_url, __( 'Your new login URL:', 'vigilante' ) );
6268 $body .= Vigilante_Email_Template::alert_box( __( 'The old login address (wp-login.php) will no longer work.', 'vigilante' ) );
6269 $body .= Vigilante_Email_Template::button( $login_url, __( 'Go to login', 'vigilante' ) );
6270
6271 $sent = 0;
6272 $failed = 0;
6273
6274 foreach ( $users as $user ) {
6275 $result = Vigilante_Email_Template::send(
6276 $user->user_email,
6277 $subject,
6278 __( 'Login URL changed', 'vigilante' ),
6279 $body
6280 );
6281 if ( $result ) {
6282 $sent++;
6283 } else {
6284 $failed++;
6285 }
6286 }
6287
6288 if ( $this->activity_log ) {
6289 $this->activity_log->log(
6290 'login',
6291 'login_url_notified',
6292 sprintf(
6293 /* translators: 1: Sent count, 2: Failed count */
6294 __( 'Login URL notification sent on save: %1$d sent, %2$d failed', 'vigilante' ),
6295 $sent,
6296 $failed
6297 )
6298 );
6299 }
6300
6301 return array( 'sent' => $sent, 'failed' => $failed );
6302 }
6303
6304 /**
6305 * Process section data maintaining proper types from defaults
6306 *
6307 * @param array $submitted_data Data submitted from form.
6308 * @param array $defaults Default values for this section.
6309 * @param array $current Current saved values.
6310 * @param string $section_name Section name for special handling.
6311 * @return array Processed data.
6312 */
6313 private function process_section_data( $submitted_data, $defaults, $current, $section_name = '' ) {
6314 // Start with defaults, then merge current saved values
6315 $result = array_replace_recursive( $defaults, $current );
6316
6317 // Process each submitted value
6318 foreach ( $submitted_data as $key => $value ) {
6319 $key = sanitize_key( $key );
6320
6321 if ( is_array( $value ) ) {
6322 // Nested array (like rate_limiting)
6323 $nested_defaults = isset( $defaults[ $key ] ) && is_array( $defaults[ $key ] ) ? $defaults[ $key ] : array();
6324 $nested_current = isset( $result[ $key ] ) && is_array( $result[ $key ] ) ? $result[ $key ] : array();
6325 $result[ $key ] = $this->process_section_data( $value, $nested_defaults, $nested_current, $key );
6326 } else {
6327 // Determine type from default value
6328 $default_value = isset( $defaults[ $key ] ) ? $defaults[ $key ] : null;
6329
6330 if ( null === $default_value ) {
6331 // No default, check current value type or use as string
6332 $current_value = isset( $current[ $key ] ) ? $current[ $key ] : null;
6333 if ( is_bool( $current_value ) ) {
6334 $result[ $key ] = in_array( $value, array( '1', 1, 'true', true ), true );
6335 } elseif ( is_int( $current_value ) ) {
6336 $result[ $key ] = intval( $value );
6337 } elseif ( is_array( $current_value ) ) {
6338 $result[ $key ] = is_string( $value ) ? array_filter( array_map( 'trim', explode( "\n", $value ) ) ) : (array) $value;
6339 } else {
6340 $result[ $key ] = sanitize_text_field( $value );
6341 }
6342 } elseif ( is_bool( $default_value ) ) {
6343 // Boolean: '1', 1, 'true' become true; '0', 0, '', 'false' become false
6344 $result[ $key ] = in_array( $value, array( '1', 1, 'true', true ), true );
6345 } elseif ( is_int( $default_value ) ) {
6346 // Integer - with special handling for time fields shown in minutes
6347 $int_value = intval( $value );
6348
6349 // Convert minutes to seconds for login_security duration fields
6350 // These are displayed as minutes in the form but stored as seconds
6351 if ( in_array( $key, array( 'lockout_duration', 'max_lockout_duration' ), true ) ) {
6352 $int_value = $int_value * 60;
6353 }
6354
6355 $result[ $key ] = $int_value;
6356 } elseif ( is_array( $default_value ) ) {
6357 // Array from textarea (e.g., IP lists)
6358 if ( is_string( $value ) ) {
6359 $result[ $key ] = array_filter( array_map( 'trim', explode( "\n", $value ) ) );
6360 } else {
6361 $result[ $key ] = (array) $value;
6362 }
6363 } else {
6364 // String - preserve newlines for textarea fields
6365 if ( is_string( $value ) && ( strpos( $value, "\n" ) !== false || strpos( $value, "\r" ) !== false ) ) {
6366 $result[ $key ] = sanitize_textarea_field( $value );
6367 } else {
6368 $result[ $key ] = sanitize_text_field( $value );
6369 }
6370 }
6371 }
6372 }
6373
6374 // Handle unchecked checkboxes: HTML forms don't submit unchecked boxes
6375 // If a boolean field exists in defaults but NOT in submitted_data, set it to false
6376 //
6377 // EXCEPTION: the top-level 'enabled' flag of every section is the
6378 // module's master switch and is controlled by the Dashboard module
6379 // toggle, NOT by a checkbox inside the section's form. Treating it
6380 // like a regular checkbox here would silently switch the module off
6381 // every time the user saves the tab — see the REST API enabled=false
6382 // regression. We only skip it at the top level (when section_name is
6383 // empty); nested 'enabled' fields like security_headers.csp.enabled
6384 // are real checkboxes and must keep the auto-unset behaviour.
6385 $preserve_top_level = array( 'enabled' );
6386
6387 foreach ( $defaults as $key => $default_value ) {
6388 if ( '' === $section_name && in_array( $key, $preserve_top_level, true ) ) {
6389 continue;
6390 }
6391 if ( is_bool( $default_value ) && ! array_key_exists( $key, $submitted_data ) ) {
6392 $result[ $key ] = false;
6393 } elseif ( is_array( $default_value ) && ! isset( $submitted_data[ $key ] ) ) {
6394 // Check if this is a flat value list (like excluded_users, enforced_roles, ip_whitelist)
6395 // vs a nested settings group (like rate_limiting, two_factor)
6396 // Flat lists: default is empty array OR all values are scalar
6397 $is_value_list = empty( $default_value );
6398 if ( ! $is_value_list ) {
6399 $is_value_list = true;
6400 foreach ( $default_value as $dv ) {
6401 if ( ! is_scalar( $dv ) ) {
6402 $is_value_list = false;
6403 break;
6404 }
6405 }
6406 }
6407
6408 if ( $is_value_list ) {
6409 // All items removed - reset to empty array
6410 $result[ $key ] = array();
6411 } else {
6412 // Nested settings group - handle boolean children
6413 foreach ( $default_value as $nested_key => $nested_default ) {
6414 if ( is_bool( $nested_default ) && isset( $result[ $key ] ) && is_array( $result[ $key ] ) ) {
6415 $nested_submitted = isset( $submitted_data[ $key ] ) && is_array( $submitted_data[ $key ] )
6416 ? $submitted_data[ $key ]
6417 : array();
6418 if ( ! array_key_exists( $nested_key, $nested_submitted ) ) {
6419 $result[ $key ][ $nested_key ] = false;
6420 }
6421 }
6422 }
6423 }
6424 }
6425 }
6426
6427 return $result;
6428 }
6429
6430 /**
6431 * AJAX: Export settings
6432 */
6433 public function ajax_export_settings() {
6434 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6435
6436 if ( ! current_user_can( 'manage_options' ) ) {
6437 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6438 }
6439
6440 $options = $this->settings->get_all_options();
6441 $filename = 'vigilante-settings-' . gmdate( 'Y-m-d-His' ) . '.json';
6442
6443 wp_send_json_success( array(
6444 'content' => wp_json_encode( $options, JSON_PRETTY_PRINT ),
6445 'filename' => $filename,
6446 ) );
6447 }
6448
6449 /**
6450 * AJAX: Import settings
6451 */
6452 public function ajax_import_settings() {
6453 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6454
6455 if ( ! current_user_can( 'manage_options' ) ) {
6456 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6457 }
6458
6459 // Accept both 'settings' (from JS) and 'content' (legacy).
6460 // Do NOT run sanitize_text_field() on the raw payload: it calls
6461 // wp_strip_all_tags() internally, which removes any "<...>" substring
6462 // and turns a valid export JSON into garbage if any stored value
6463 // contains < or > (htaccess snippets, email templates, etc.). The
6464 // real sanitization happens after json_decode(), via map_deep() on
6465 // the parsed array.
6466 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
6467 $content = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : '';
6468 if ( empty( $content ) ) {
6469 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
6470 $content = isset( $_POST['content'] ) ? wp_unslash( $_POST['content'] ) : '';
6471 }
6472
6473 if ( empty( $content ) || ! is_string( $content ) ) {
6474 wp_send_json_error( __( 'No content provided.', 'vigilante' ) );
6475 }
6476
6477 $imported = json_decode( $content, true );
6478
6479 if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $imported ) ) {
6480 wp_send_json_error( __( 'Invalid JSON format.', 'vigilante' ) );
6481 }
6482
6483 // Sanitize imported data recursively
6484 $imported = map_deep( $imported, 'sanitize_text_field' );
6485
6486 // Validate structure
6487 $defaults = $this->settings->get_default_options();
6488 $merged = array_replace_recursive( $defaults, $imported );
6489
6490 // Save
6491 update_option( Vigilante_Settings::OPTION_NAME, $merged );
6492 $this->settings->clear_cache();
6493
6494 // Re-evaluate the active preset marker. The imported config may match
6495 // a known preset exactly, partially, or not at all — without this step
6496 // the dashboard would keep showing whatever preset was active before
6497 // the import even if the new config no longer matches it.
6498 $matched_preset = $this->detect_matching_preset( $merged );
6499 if ( null === $matched_preset ) {
6500 delete_option( 'vigilante_active_preset' );
6501 } else {
6502 update_option( 'vigilante_active_preset', $matched_preset );
6503 }
6504
6505 // Apply file changes after import
6506 $this->apply_all_file_changes( $merged );
6507
6508 // Refresh the Security Analyzer score so the dashboard widget reflects
6509 // the imported config rather than the pre-import scan. Reuse the
6510 // post-Under-Attack scan hook (same job: full scan, async).
6511 if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) {
6512 wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' );
6513 }
6514
6515 wp_send_json_success( __( 'Settings imported successfully.', 'vigilante' ) );
6516 }
6517
6518 /**
6519 * Detect whether a vigilante_options array matches a known preset.
6520 *
6521 * A preset matches when every field the preset explicitly declares is
6522 * present in the config with the same (normalised) value. Fields outside
6523 * the preset are ignored — they may have been modified by the user before
6524 * applying the preset and do not invalidate the match. This mirrors how
6525 * apply_preset() now layers presets on top of the user's existing config.
6526 *
6527 * @param array $options The current/imported vigilante_options.
6528 * @return string|null Preset id ('standard', 'maximum') or null if custom.
6529 */
6530 private function detect_matching_preset( $options ) {
6531 if ( ! is_array( $options ) ) {
6532 return null;
6533 }
6534
6535 $presets = $this->settings->get_presets();
6536
6537 foreach ( $presets as $preset_id => $preset_data ) {
6538 unset( $preset_data['name'], $preset_data['description'] );
6539 if ( $this->preset_subset_matches( $preset_data, $options ) ) {
6540 return $preset_id;
6541 }
6542 }
6543
6544 return null;
6545 }
6546
6547 /**
6548 * Check whether every leaf value inside $preset_subset exists with the
6549 * same (normalised) value at the same path inside $config.
6550 *
6551 * @param mixed $preset_subset Branch of the preset definition.
6552 * @param mixed $config Same branch in the live/imported config.
6553 * @return bool
6554 */
6555 private function preset_subset_matches( $preset_subset, $config ) {
6556 if ( is_array( $preset_subset ) ) {
6557 if ( ! is_array( $config ) ) {
6558 return false;
6559 }
6560 foreach ( $preset_subset as $key => $value ) {
6561 if ( ! array_key_exists( $key, $config ) ) {
6562 return false;
6563 }
6564 if ( ! $this->preset_subset_matches( $value, $config[ $key ] ) ) {
6565 return false;
6566 }
6567 }
6568 return true;
6569 }
6570
6571 return $this->normalise_scalar_for_compare( $preset_subset ) === $this->normalise_scalar_for_compare( $config );
6572 }
6573
6574 /**
6575 * Normalise a scalar value so that the variants WordPress and the form
6576 * layer routinely produce ('1' / 1 / true → "1"; '' / '0' / 0 / false /
6577 * null → "") compare equal. Other values become strings unchanged.
6578 *
6579 * @param mixed $value
6580 * @return string
6581 */
6582 private function normalise_scalar_for_compare( $value ) {
6583 if ( is_bool( $value ) ) {
6584 return $value ? '1' : '';
6585 }
6586 if ( null === $value ) {
6587 return '';
6588 }
6589 if ( is_int( $value ) || is_float( $value ) ) {
6590 return (string) $value;
6591 }
6592 if ( is_string( $value ) ) {
6593 if ( 'true' === $value ) {
6594 return '1';
6595 }
6596 if ( 'false' === $value ) {
6597 return '';
6598 }
6599 return $value;
6600 }
6601 // Arrays and objects shouldn't reach here (handled by recursion above),
6602 // but if they do, fall back to a stable comparable representation.
6603 return wp_json_encode( $value );
6604 }
6605
6606 /**
6607 * AJAX: Apply preset
6608 */
6609 public function ajax_apply_preset() {
6610 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6611
6612 if ( ! current_user_can( 'manage_options' ) ) {
6613 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6614 }
6615
6616 $preset = isset( $_POST['preset'] ) ? sanitize_key( $_POST['preset'] ) : '';
6617
6618 // Handle reset to defaults
6619 if ( 'reset' === $preset ) {
6620 $defaults = $this->settings->get_default_options();
6621 update_option( Vigilante_Settings::OPTION_NAME, $defaults );
6622 $this->settings->clear_cache();
6623
6624 // Clear active preset
6625 update_option( 'vigilante_active_preset', '' );
6626
6627 // Apply file changes after reset
6628 $this->apply_all_file_changes( $defaults );
6629
6630 wp_send_json_success( __( 'Settings reset to defaults.', 'vigilante' ) );
6631 return;
6632 }
6633
6634 $presets = $this->settings->get_presets();
6635
6636 if ( ! isset( $presets[ $preset ] ) ) {
6637 wp_send_json_error( __( 'Invalid preset.', 'vigilante' ) );
6638 }
6639
6640 $preset_options = $presets[ $preset ];
6641 unset( $preset_options['name'], $preset_options['description'] );
6642
6643 // Layer the preset on top of the user's CURRENT configuration, not on
6644 // top of defaults. This way applying a preset only changes the fields
6645 // the preset explicitly mentions; everything else stays as the user
6646 // had it. For example, applying Maximum will not flip HSTS off if the
6647 // user had it on — Maximum doesn't touch HSTS, so it's left alone.
6648 // Use "Reset to Defaults" if a clean slate is needed.
6649 $current = get_option( Vigilante_Settings::OPTION_NAME, array() );
6650 if ( ! is_array( $current ) ) {
6651 $current = array();
6652 }
6653 // Make sure all known keys exist before merging — array_replace_recursive
6654 // does not invent keys that are missing on both sides.
6655 $current = array_replace_recursive( $this->settings->get_default_options(), $current );
6656
6657 $merged = array_replace_recursive( $current, $preset_options );
6658
6659 update_option( Vigilante_Settings::OPTION_NAME, $merged );
6660 $this->settings->clear_cache();
6661
6662 // Save active preset
6663 update_option( 'vigilante_active_preset', $preset );
6664
6665 // Apply file changes after preset
6666 $this->apply_all_file_changes( $merged );
6667
6668 wp_send_json_success( __( 'Preset applied successfully.', 'vigilante' ) );
6669 }
6670
6671 /**
6672 * AJAX: Reset a specific section to defaults
6673 */
6674 public function ajax_reset_section() {
6675 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6676
6677 if ( ! current_user_can( 'manage_options' ) ) {
6678 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6679 }
6680
6681 $section = isset( $_POST['section'] ) ? sanitize_key( $_POST['section'] ) : '';
6682
6683 if ( empty( $section ) ) {
6684 wp_send_json_error( __( 'No section specified.', 'vigilante' ) );
6685 }
6686
6687 // Get current options and defaults
6688 $current_options = $this->settings->get_all_options();
6689 $defaults = $this->settings->get_default_options();
6690
6691 // Check if section exists in defaults
6692 if ( ! isset( $defaults[ $section ] ) ) {
6693 wp_send_json_error( __( 'Invalid section.', 'vigilante' ) );
6694 }
6695
6696 // Reset only this section to defaults
6697 $current_options[ $section ] = $defaults[ $section ];
6698
6699 // Save
6700 update_option( Vigilante_Settings::OPTION_NAME, $current_options );
6701 $this->settings->clear_cache();
6702
6703 // Apply file changes if needed
6704 $this->apply_section_changes( $section, $current_options );
6705
6706 wp_send_json_success( array(
6707 'message' => __( 'Section reset to defaults.', 'vigilante' ),
6708 'section' => $section,
6709 'reload' => true,
6710 ) );
6711 }
6712
6713 /**
6714 * AJAX: Clear lockouts
6715 */
6716 public function ajax_clear_lockouts() {
6717 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6718
6719 if ( ! current_user_can( 'manage_options' ) ) {
6720 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6721 }
6722
6723 $ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : '';
6724
6725 if ( ! empty( $ip ) ) {
6726 $this->database->clear_lockout( $ip );
6727 } else {
6728 $this->database->clear_all_lockouts();
6729 }
6730
6731 wp_send_json_success( __( 'Lockouts cleared.', 'vigilante' ) );
6732 }
6733
6734 /**
6735 * AJAX: Clear logs
6736 */
6737 public function ajax_clear_logs() {
6738 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6739
6740 if ( ! current_user_can( 'manage_options' ) ) {
6741 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6742 }
6743
6744 if ( $this->activity_log ) {
6745 $result = $this->activity_log->clear_all_logs();
6746 if ( $result ) {
6747 wp_send_json_success( __( 'Logs cleared.', 'vigilante' ) );
6748 } else {
6749 wp_send_json_error( __( 'Failed to clear logs.', 'vigilante' ) );
6750 }
6751 } else {
6752 wp_send_json_error( __( 'Activity log not available.', 'vigilante' ) );
6753 }
6754 }
6755
6756 /**
6757 * AJAX: Run file integrity scan.
6758 *
6759 * Triggers the file integrity scan, which now also runs the closed plugins
6760 * check at the end when the `check_closed_plugins` toggle is on. Activity
6761 * log is passed through so Security Audit entries (both file-level and
6762 * plugin-status) are recorded from this entry point.
6763 */
6764 public function ajax_run_scan() {
6765 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6766
6767 if ( ! current_user_can( 'manage_options' ) ) {
6768 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6769 }
6770
6771 // Clear previous results before running new scan
6772 delete_option( 'vigilante_last_integrity_results' );
6773 delete_option( 'vigilante_last_integrity_scan' );
6774
6775 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log );
6776 $results = $file_integrity->run_scan();
6777
6778 // Save new results
6779 update_option( 'vigilante_last_integrity_scan', time() );
6780 update_option( 'vigilante_last_integrity_results', $results );
6781
6782 wp_send_json_success( array(
6783 'message' => __( 'Scan completed.', 'vigilante' ),
6784 'results' => $results,
6785 'ignored_count' => count( get_option( 'vigilante_ignored_files', array() ) ),
6786 ) );
6787 }
6788
6789 /**
6790 * AJAX: Clear scan results.
6791 *
6792 * Wipes visually everything inside "Last Scan Results": file scan findings
6793 * (Suspicious / Modified / Extra / Critical Config), file hashes and the
6794 * Closed + Removed Plugins block.
6795 *
6796 * Implementation detail to keep persistence intact:
6797 * - We delete the file scan options + hashes outright.
6798 * - For plugin status we ONLY delete the last_check timestamp — the state
6799 * map and the ignore list are preserved in DB. The render gates the
6800 * plugin_status subsections on last_check > 0, so they hide after
6801 * Clear (visual reset) and reappear on the next Run Scan Now with the
6802 * state intact. This avoids degrading a 'removed' slug (404 without
6803 * metadata) back to 'not_in_repo' on the next scan, which would
6804 * silently lose the alert.
6805 *
6806 * The Ignored Files list and the Ignored Closed + Removed Plugins list
6807 * are preserved on purpose; each has its own explicit "Clear All …"
6808 * button.
6809 */
6810 public function ajax_clear_scan() {
6811 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6812
6813 if ( ! current_user_can( 'manage_options' ) ) {
6814 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6815 }
6816
6817 delete_option( 'vigilante_last_integrity_results' );
6818 delete_option( 'vigilante_last_integrity_scan' );
6819
6820 if ( $this->database ) {
6821 $this->database->clear_file_hashes();
6822 }
6823
6824 // Visual reset of plugin_status block without touching the state map
6825 // or the ignored list. See PHPDoc above for the rationale.
6826 delete_option( 'vigilante_plugin_status_last_check' );
6827
6828 wp_send_json_success( __( 'Scan results cleared.', 'vigilante' ) );
6829 }
6830
6831 /**
6832 * AJAX: Ignore a file from scan results
6833 */
6834 public function ajax_ignore_file() {
6835 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6836
6837 if ( ! current_user_can( 'manage_options' ) ) {
6838 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6839 }
6840
6841 $file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : '';
6842
6843 if ( empty( $file ) ) {
6844 wp_send_json_error( __( 'No file specified.', 'vigilante' ) );
6845 }
6846
6847 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
6848 $file_integrity->ignore_file( $file );
6849
6850 // Also remove the file from stored scan results so UI updates
6851 $results = get_option( 'vigilante_last_integrity_results' );
6852 if ( $results ) {
6853 foreach ( array( 'modified', 'suspicious', 'extra' ) as $category ) {
6854 if ( ! empty( $results[ $category ] ) ) {
6855 $results[ $category ] = array_values(
6856 array_filter(
6857 $results[ $category ],
6858 function ( $item ) use ( $file ) {
6859 return ( is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item ) !== $file;
6860 }
6861 )
6862 );
6863 }
6864 }
6865 update_option( 'vigilante_last_integrity_results', $results );
6866 }
6867
6868 wp_send_json_success( __( 'File added to ignored list.', 'vigilante' ) );
6869 }
6870
6871 /**
6872 * AJAX: Stop ignoring a file
6873 */
6874 public function ajax_unignore_file() {
6875 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6876
6877 if ( ! current_user_can( 'manage_options' ) ) {
6878 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6879 }
6880
6881 $file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : '';
6882
6883 if ( empty( $file ) ) {
6884 wp_send_json_error( __( 'No file specified.', 'vigilante' ) );
6885 }
6886
6887 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
6888 $file_integrity->unignore_file( $file );
6889
6890 wp_send_json_success( __( 'File removed from ignored list.', 'vigilante' ) );
6891 }
6892
6893 /**
6894 * AJAX: Bulk ignore multiple files at once
6895 *
6896 * Processes a single batch into ignored list and prunes them from the
6897 * stored scan results so the UI updates without a re-scan.
6898 */
6899 public function ajax_bulk_ignore_files() {
6900 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6901
6902 if ( ! current_user_can( 'manage_options' ) ) {
6903 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6904 }
6905
6906 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below per-item.
6907 $raw_files = isset( $_POST['files'] ) ? wp_unslash( $_POST['files'] ) : array();
6908 if ( ! is_array( $raw_files ) ) {
6909 wp_send_json_error( __( 'Invalid request.', 'vigilante' ) );
6910 }
6911
6912 $files = array();
6913 foreach ( $raw_files as $f ) {
6914 $clean = sanitize_text_field( $f );
6915 if ( '' !== $clean ) {
6916 $files[] = $clean;
6917 }
6918 }
6919
6920 if ( empty( $files ) ) {
6921 wp_send_json_error( __( 'No files selected.', 'vigilante' ) );
6922 }
6923
6924 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
6925 $count = 0;
6926 foreach ( $files as $file ) {
6927 $file_integrity->ignore_file( $file );
6928 $count++;
6929 }
6930
6931 // Also prune the stored scan results so the UI matches the new ignore list.
6932 $results = get_option( 'vigilante_last_integrity_results' );
6933 if ( $results ) {
6934 $files_set = array_flip( $files );
6935 foreach ( array( 'modified', 'suspicious', 'extra' ) as $category ) {
6936 if ( ! empty( $results[ $category ] ) ) {
6937 $results[ $category ] = array_values(
6938 array_filter(
6939 $results[ $category ],
6940 function ( $item ) use ( $files_set ) {
6941 $path = is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item;
6942 return ! isset( $files_set[ $path ] );
6943 }
6944 )
6945 );
6946 }
6947 }
6948 update_option( 'vigilante_last_integrity_results', $results );
6949 }
6950
6951 wp_send_json_success(
6952 array(
6953 'count' => $count,
6954 'message' => sprintf(
6955 /* translators: %d: number of files added to the ignored list */
6956 _n( '%d file added to ignored list.', '%d files added to ignored list.', $count, 'vigilante' ),
6957 $count
6958 ),
6959 )
6960 );
6961 }
6962
6963 /**
6964 * AJAX: Bulk un-ignore multiple files at once
6965 */
6966 public function ajax_bulk_unignore_files() {
6967 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6968
6969 if ( ! current_user_can( 'manage_options' ) ) {
6970 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6971 }
6972
6973 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below per-item.
6974 $raw_files = isset( $_POST['files'] ) ? wp_unslash( $_POST['files'] ) : array();
6975 if ( ! is_array( $raw_files ) ) {
6976 wp_send_json_error( __( 'Invalid request.', 'vigilante' ) );
6977 }
6978
6979 $files = array();
6980 foreach ( $raw_files as $f ) {
6981 $clean = sanitize_text_field( $f );
6982 if ( '' !== $clean ) {
6983 $files[] = $clean;
6984 }
6985 }
6986
6987 if ( empty( $files ) ) {
6988 wp_send_json_error( __( 'No files selected.', 'vigilante' ) );
6989 }
6990
6991 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
6992 $count = 0;
6993 foreach ( $files as $file ) {
6994 $file_integrity->unignore_file( $file );
6995 $count++;
6996 }
6997
6998 wp_send_json_success(
6999 array(
7000 'count' => $count,
7001 'message' => sprintf(
7002 /* translators: %d: number of files removed from the ignored list */
7003 _n( '%d file removed from ignored list.', '%d files removed from ignored list.', $count, 'vigilante' ),
7004 $count
7005 ),
7006 )
7007 );
7008 }
7009
7010 /**
7011 * AJAX: Clear all ignored files
7012 */
7013 public function ajax_clear_ignored() {
7014 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7015
7016 if ( ! current_user_can( 'manage_options' ) ) {
7017 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7018 }
7019
7020 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
7021 $file_integrity->clear_ignored_files();
7022
7023 wp_send_json_success( __( 'Ignored files list cleared.', 'vigilante' ) );
7024 }
7025
7026 /**
7027 * AJAX: Ignore a closed/removed plugin slug so it stops appearing in the
7028 * main list and email digests. The plugin keeps running on the site;
7029 * silencing is purely cosmetic and reversible.
7030 */
7031 public function ajax_ignore_closed_plugin() {
7032 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7033
7034 if ( ! current_user_can( 'manage_options' ) ) {
7035 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7036 }
7037
7038 $slug = isset( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
7039 if ( '' === $slug ) {
7040 wp_send_json_error( __( 'No slug specified.', 'vigilante' ) );
7041 }
7042
7043 if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) {
7044 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
7045 }
7046 $checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
7047 $checker->ignore_slug( $slug );
7048
7049 wp_send_json_success( __( 'Plugin added to the ignored list.', 'vigilante' ) );
7050 }
7051
7052 /**
7053 * AJAX: Stop ignoring a previously-ignored closed/removed plugin slug.
7054 */
7055 public function ajax_unignore_closed_plugin() {
7056 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7057
7058 if ( ! current_user_can( 'manage_options' ) ) {
7059 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7060 }
7061
7062 $slug = isset( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
7063 if ( '' === $slug ) {
7064 wp_send_json_error( __( 'No slug specified.', 'vigilante' ) );
7065 }
7066
7067 if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) {
7068 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
7069 }
7070 $checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
7071 $checker->unignore_slug( $slug );
7072
7073 wp_send_json_success( __( 'Plugin removed from the ignored list.', 'vigilante' ) );
7074 }
7075
7076 /**
7077 * AJAX: Clear the entire ignored-closed-plugins list.
7078 */
7079 public function ajax_clear_ignored_closed_plugins() {
7080 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7081
7082 if ( ! current_user_can( 'manage_options' ) ) {
7083 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7084 }
7085
7086 if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) {
7087 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
7088 }
7089 $checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
7090 $checker->clear_ignored();
7091
7092 wp_send_json_success( __( 'Ignored closed plugins list cleared.', 'vigilante' ) );
7093 }
7094
7095 /**
7096 * AJAX: Test security headers
7097 */
7098 public function ajax_test_headers() {
7099 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7100
7101 if ( ! current_user_can( 'manage_options' ) ) {
7102 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7103 }
7104
7105 // Get security grade from settings (not from actual HTTP request)
7106 $security_headers = new Vigilante_Security_Headers( $this->settings );
7107 $results = $security_headers->get_security_grade();
7108
7109 wp_send_json_success( $results );
7110 }
7111
7112 /**
7113 * Sanitize section data (kept for compatibility)
7114 *
7115 * @param string $section Section name.
7116 * @param array $data Section data.
7117 * @return array
7118 */
7119 private function sanitize_section_data( $section, $data ) {
7120 $sanitized = array();
7121
7122 foreach ( $data as $key => $value ) {
7123 $key = sanitize_key( $key );
7124
7125 if ( is_array( $value ) ) {
7126 $sanitized[ $key ] = $this->sanitize_section_data( $key, $value );
7127 } elseif ( is_numeric( $value ) ) {
7128 $sanitized[ $key ] = intval( $value );
7129 } else {
7130 $sanitized[ $key ] = sanitize_text_field( $value );
7131 }
7132 }
7133
7134 return $sanitized;
7135 }
7136
7137 /**
7138 * Apply changes after saving settings
7139 *
7140 * @param string $section Section that was updated.
7141 * @param array $all_options All options.
7142 */
7143 private function apply_section_changes( $section, $all_options ) {
7144 // Create fresh settings instance to ensure we have the latest data
7145 $fresh_settings = new Vigilante_Settings();
7146
7147 // The shared Vigilant .htaccess block carries BOTH the firewall's
7148 // file-protection rules and the server-signature / fingerprinting rules
7149 // that are configured under Security Headers. So it must be regenerated
7150 // whenever either section (or the module toggles) changes, not only on
7151 // the firewall save — otherwise toggling "Hide server signature" or
7152 // "Remove fingerprinting headers" never reaches the .htaccess.
7153 if ( in_array( $section, array( 'firewall', 'security_headers', 'modules' ), true ) ) {
7154 $htaccess = new Vigilante_Htaccess_Protection( $fresh_settings );
7155 $sh = $fresh_settings->get_section( 'security_headers' );
7156 $needs_htaccess_block = ! empty( $all_options['modules']['firewall'] )
7157 || ! empty( $sh['hide_server_signature'] )
7158 || ! empty( $sh['remove_fingerprinting_headers'] );
7159
7160 if ( $needs_htaccess_block ) {
7161 $htaccess->apply_rules();
7162 } else {
7163 $htaccess->remove_rules();
7164 }
7165 }
7166
7167 // Regenerate the HTTP security headers (sent by PHP) for the headers section.
7168 if ( 'security_headers' === $section || 'modules' === $section ) {
7169 $security_headers = new Vigilante_Security_Headers( $fresh_settings );
7170 $headers_enabled = ! empty( $all_options['modules']['security_headers'] );
7171
7172 if ( $headers_enabled ) {
7173 $security_headers->apply_rules();
7174 } else {
7175 $security_headers->remove_rules();
7176 }
7177 }
7178
7179 // Regenerate wp-config for wp_hardening section
7180 if ( 'wp_hardening' === $section || 'modules' === $section ) {
7181 $wpconfig = new Vigilante_Wpconfig_Security( $fresh_settings );
7182 $hardening_enabled = ! empty( $all_options['modules']['wp_hardening'] );
7183
7184 if ( $hardening_enabled ) {
7185 $wpconfig->apply_security_constants();
7186 } else {
7187 $wpconfig->remove_constants();
7188 }
7189
7190 // Apply WordPress options for comments/pingbacks
7191 $hardening_options = $all_options['wp_hardening'] ?? array();
7192
7193 if ( $hardening_enabled ) {
7194 // Pingbacks
7195 if ( ! empty( $hardening_options['disable_pingbacks'] ) ) {
7196 update_option( 'default_pingback_flag', 0 );
7197 } else {
7198 // Restore default: pingbacks enabled
7199 update_option( 'default_pingback_flag', 1 );
7200 }
7201
7202 // Trackbacks and ping status
7203 // Only close if either pingbacks OR trackbacks are disabled
7204 if ( ! empty( $hardening_options['disable_pingbacks'] ) || ! empty( $hardening_options['disable_trackbacks'] ) ) {
7205 update_option( 'default_ping_status', 'closed' );
7206 } else {
7207 // Restore default: pings open
7208 update_option( 'default_ping_status', 'open' );
7209 }
7210
7211 // Comment moderation
7212 if ( ! empty( $hardening_options['require_comment_moderation'] ) ) {
7213 update_option( 'comment_moderation', 1 );
7214 } else {
7215 // Restore default: no moderation required
7216 update_option( 'comment_moderation', 0 );
7217 }
7218 }
7219 }
7220
7221 // Trim activity log entries immediately when limits change
7222 if ( 'activity_log' === $section && $this->activity_log ) {
7223 $this->activity_log->cleanup_old_logs();
7224 }
7225
7226 // Flush rewrite rules if login URL changed
7227 if ( 'login_security' === $section ) {
7228 $login_options = $all_options['login_security'] ?? array();
7229 if ( ! empty( $login_options['custom_login_url'] ) ) {
7230 delete_option( 'vigilante_login_rules_version' );
7231 }
7232 }
7233
7234 // Log the settings change with readable section name
7235 if ( $this->activity_log ) {
7236 $section_names = array(
7237 'firewall' => __( 'Firewall', 'vigilante' ),
7238 'login_security' => __( 'Login Security', 'vigilante' ),
7239 'security_headers' => __( 'Security Headers', 'vigilante' ),
7240 'rest_api_security'=> __( 'REST API Security', 'vigilante' ),
7241 'user_security' => __( 'User Security', 'vigilante' ),
7242 'wp_hardening' => __( 'WP Hardening', 'vigilante' ),
7243 'activity_log' => __( 'Security Audit', 'vigilante' ),
7244 'file_integrity' => __( 'File Integrity', 'vigilante' ),
7245 'email' => __( 'Notification Settings', 'vigilante' ),
7246 'backup' => __( 'Backup', 'vigilante' ),
7247 'advanced' => __( 'Advanced', 'vigilante' ),
7248 'modules' => __( 'Modules', 'vigilante' ),
7249 );
7250 $display_name = isset( $section_names[ $section ] ) ? $section_names[ $section ] : $section;
7251
7252 $this->activity_log->log(
7253 'settings',
7254 'settings_updated',
7255 sprintf(
7256 /* translators: %s: Section name */
7257 __( 'Settings updated: %s', 'vigilante' ),
7258 $display_name
7259 ),
7260 array( 'section' => $section ),
7261 'info'
7262 );
7263 }
7264 }
7265
7266 /**
7267 * Apply all file changes (htaccess, wp-config) based on current options
7268 *
7269 * Used after preset, import, or reset operations
7270 *
7271 * @param array $all_options All plugin options.
7272 */
7273 private function apply_all_file_changes( $all_options ) {
7274 // Refresh settings cache first
7275 $this->settings->clear_cache();
7276
7277 // Create fresh settings instance
7278 $fresh_settings = new Vigilante_Settings();
7279
7280 // Apply firewall htaccess changes
7281 $htaccess = new Vigilante_Htaccess_Protection( $fresh_settings );
7282 $firewall_enabled = ! empty( $all_options['modules']['firewall'] );
7283
7284 if ( $firewall_enabled ) {
7285 $htaccess->apply_rules();
7286 } else {
7287 $htaccess->remove_rules();
7288 }
7289
7290 // Apply security headers htaccess changes
7291 $security_headers = new Vigilante_Security_Headers( $fresh_settings );
7292 $headers_enabled = ! empty( $all_options['modules']['security_headers'] );
7293
7294 if ( $headers_enabled ) {
7295 $security_headers->apply_rules();
7296 } else {
7297 $security_headers->remove_rules();
7298 }
7299
7300 // Apply wp-config changes
7301 $wpconfig = new Vigilante_Wpconfig_Security( $fresh_settings );
7302 $hardening_enabled = ! empty( $all_options['modules']['wp_hardening'] );
7303
7304 if ( $hardening_enabled ) {
7305 $wpconfig->apply_security_constants();
7306 } else {
7307 $wpconfig->remove_constants();
7308 }
7309
7310 // Apply WordPress options for comments/pingbacks
7311 $hardening_options = $all_options['wp_hardening'] ?? array();
7312
7313 if ( $hardening_enabled ) {
7314 // Pingbacks
7315 if ( ! empty( $hardening_options['disable_pingbacks'] ) ) {
7316 update_option( 'default_pingback_flag', 0 );
7317 } else {
7318 // Restore default: pingbacks enabled
7319 update_option( 'default_pingback_flag', 1 );
7320 }
7321
7322 // Trackbacks and ping status
7323 // Only close if either pingbacks OR trackbacks are disabled
7324 if ( ! empty( $hardening_options['disable_pingbacks'] ) || ! empty( $hardening_options['disable_trackbacks'] ) ) {
7325 update_option( 'default_ping_status', 'closed' );
7326 } else {
7327 // Restore default: pings open
7328 update_option( 'default_ping_status', 'open' );
7329 }
7330
7331 // Comment moderation
7332 if ( ! empty( $hardening_options['require_comment_moderation'] ) ) {
7333 update_option( 'comment_moderation', 1 );
7334 } else {
7335 // Restore default: no moderation required
7336 update_option( 'comment_moderation', 0 );
7337 }
7338 }
7339
7340 // Log the change
7341 if ( $this->activity_log ) {
7342 $this->activity_log->log(
7343 'settings',
7344 'bulk_settings_applied',
7345 __( 'Bulk settings applied (preset/import/reset)', 'vigilante' ),
7346 array(),
7347 'info'
7348 );
7349 }
7350 }
7351 }