PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.9.8
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.9.8
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.8, at admin/class-admin.php

7,656 lines 487.0 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 * 2.9.8: the mixed content handling changes shape. "Upgrade Insecure
350 * Requests" becomes a setting of its own, and Fix Mixed Content ships
351 * off, where before it shipped on and carried the directive with it.
352 * Both have to be written down for sites that are updating, so their
353 * pages keep loading exactly what they loaded yesterday.
354 *
355 * Read the RAW stored options, not get_section(): that one merges the
356 * defaults, so a site that never stored the key would be read with the
357 * new default and silently lose the behaviour it had. Absent means the
358 * site was running on the old default, which was on.
359 */
360 if ( version_compare( $db_version, '2.9.8', '<' ) ) {
361 $raw = get_option( Vigilante_Settings::OPTION_NAME, array() );
362 $stored = ( is_array( $raw ) && isset( $raw['security_headers'] ) && is_array( $raw['security_headers'] ) ) ? $raw['security_headers'] : array();
363 $had_fix = array_key_exists( 'fix_mixed_content', $stored ) ? ! empty( $stored['fix_mixed_content'] ) : true;
364
365 $this->settings->update_section(
366 'security_headers',
367 array(
368 'fix_mixed_content' => $had_fix,
369 'upgrade_insecure_requests' => $had_fix,
370 )
371 );
372
373 update_option( 'vigilante_db_version', '2.9.8' );
374 }
375 }
376
377 /**
378 * Migration: Remove orphaned email fields from saved options
379 *
380 * v1.10.0 centralized notification recipients into email section.
381 * Old per-module notify_email fields and dead email section fields
382 * are removed to avoid confusion.
383 */
384 private function migrate_cleanup_email_fields() {
385 $options = get_option( Vigilante_Settings::OPTION_NAME, array() );
386 $modified = false;
387
388 // Remove orphaned fields from email section
389 $dead_email_keys = array( 'enabled', 'from_name', 'from_email', 'admin_email', 'send_activation_email', 'custom_email' );
390 if ( isset( $options['email'] ) && is_array( $options['email'] ) ) {
391 foreach ( $dead_email_keys as $key ) {
392 if ( array_key_exists( $key, $options['email'] ) ) {
393 unset( $options['email'][ $key ] );
394 $modified = true;
395 }
396 }
397 // Ensure new fields exist with defaults
398 if ( ! array_key_exists( 'send_to_admin_email', $options['email'] ) ) {
399 $options['email']['send_to_admin_email'] = true;
400 $modified = true;
401 }
402 if ( ! array_key_exists( 'additional_recipients', $options['email'] ) ) {
403 $options['email']['additional_recipients'] = '';
404 $modified = true;
405 }
406 }
407
408 // Remove notify_email from login_security
409 if ( isset( $options['login_security']['notify_email'] ) ) {
410 unset( $options['login_security']['notify_email'] );
411 $modified = true;
412 }
413
414 // Remove notify_email from file_integrity
415 if ( isset( $options['file_integrity']['notify_email'] ) ) {
416 unset( $options['file_integrity']['notify_email'] );
417 $modified = true;
418 }
419
420 if ( $modified ) {
421 update_option( Vigilante_Settings::OPTION_NAME, $options );
422 // Clear settings cache so the plugin uses clean data immediately
423 $this->settings->clear_cache();
424 }
425 }
426
427 /**
428 * Migration: Fix IP whitelist/blacklist entries merged into single line
429 *
430 * Prior to 1.2.3, sanitize_text_field() stripped newlines from textarea data,
431 * causing multiple IPs to be stored as a single space-separated string.
432 */
433 private function migrate_fix_ip_lists() {
434 $options = get_option( Vigilante_Settings::OPTION_NAME, array() );
435 $fixed = false;
436
437 foreach ( array( 'ip_whitelist', 'ip_blacklist' ) as $key ) {
438 if ( ! empty( $options['firewall'][ $key ] ) && is_array( $options['firewall'][ $key ] ) ) {
439 $new_list = array();
440 foreach ( $options['firewall'][ $key ] as $entry ) {
441 // Split entries that were joined by spaces
442 $parts = preg_split( '/\s+/', trim( $entry ) );
443 foreach ( $parts as $part ) {
444 $part = trim( $part );
445 if ( '' !== $part ) {
446 $new_list[] = $part;
447 }
448 }
449 }
450 if ( count( $new_list ) !== count( $options['firewall'][ $key ] ) ) {
451 $options['firewall'][ $key ] = array_unique( $new_list );
452 $fixed = true;
453 }
454 }
455 }
456
457 if ( $fixed ) {
458 update_option( Vigilante_Settings::OPTION_NAME, $options );
459 // Clear settings cache so changes take effect immediately
460 $this->settings->clear_cache();
461 }
462 }
463
464 /**
465 * Add admin menu page in last position
466 */
467 public function add_menu() {
468 $menu_title = __( 'Vigilant', 'vigilante' );
469
470 // Count pending approvals (separate concern, always red if present)
471 $pending_count = $this->get_pending_approvals_count();
472
473 // Get security issues with severity
474 $security_status = $this->get_security_status_for_badge();
475
476 // Total count for badge
477 $total_badge = $pending_count + $security_status['count'];
478
479 if ( $total_badge > 0 ) {
480 // Determine badge color:
481 // - Red (awaiting-mod): pending approvals OR critical modules disabled
482 // - Orange (update-plugins): only non-critical modules disabled
483 if ( $pending_count > 0 || $security_status['has_critical'] ) {
484 $badge_class = 'awaiting-mod';
485 } else {
486 $badge_class = 'update-plugins vigilante-badge-warning';
487 }
488
489 $menu_title .= sprintf(
490 ' <span class="%s count-%d"><span class="pending-count">%d</span></span>',
491 esc_attr( $badge_class ),
492 $total_badge,
493 $total_badge
494 );
495 }
496
497 add_menu_page(
498 __( 'Vigilant', 'vigilante' ),
499 $menu_title,
500 'manage_options',
501 'vigilante',
502 array( $this, 'render_settings_page' ),
503 'dashicons-shield',
504 999
505 );
506
507 // Rename auto-generated first submenu to "Dashboard"
508 add_submenu_page(
509 'vigilante',
510 __( 'Dashboard', 'vigilante' ),
511 __( 'Dashboard', 'vigilante' ),
512 'manage_options',
513 'vigilante',
514 array( $this, 'render_settings_page' )
515 );
516
517 // Security Audit shortcut
518 add_submenu_page(
519 'vigilante',
520 __( 'Security Audit', 'vigilante' ),
521 __( 'Security Audit', 'vigilante' ),
522 'manage_options',
523 'vigilante-activity-log',
524 array( $this, 'redirect_to_tab' )
525 );
526
527 // File Integrity shortcut
528 add_submenu_page(
529 'vigilante',
530 __( 'File Integrity', 'vigilante' ),
531 __( 'File Integrity', 'vigilante' ),
532 'manage_options',
533 'vigilante-file-integrity',
534 array( $this, 'redirect_to_tab' )
535 );
536 }
537
538 /**
539 * Redirect submenu shortcuts early, before headers are sent
540 */
541 public function redirect_submenu_shortcuts() {
542 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Just reading page slug for redirect
543 $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
544
545 $tab_map = array(
546 'vigilante-activity-log' => 'activity-log',
547 'vigilante-file-integrity' => 'file-integrity',
548 );
549
550 if ( isset( $tab_map[ $page ] ) ) {
551 wp_safe_redirect( admin_url( 'admin.php?page=vigilante&tab=' . $tab_map[ $page ] ) );
552 exit;
553 }
554 }
555
556 /**
557 * Fallback redirect for submenu shortcuts (JS-based)
558 */
559 public function redirect_to_tab() {
560 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Just reading page slug for redirect
561 $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
562
563 $tab_map = array(
564 'vigilante-activity-log' => 'activity-log',
565 'vigilante-file-integrity' => 'file-integrity',
566 );
567
568 if ( isset( $tab_map[ $page ] ) ) {
569 $url = admin_url( 'admin.php?page=vigilante&tab=' . $tab_map[ $page ] );
570 echo '<script>window.location.replace(' . wp_json_encode( esc_url( $url ) ) . ');</script>';
571 }
572 }
573
574 /**
575 * Highlight the correct submenu item based on active tab
576 *
577 * @param string $submenu_file Current submenu file.
578 * @return string
579 */
580 public function highlight_submenu_tab( $submenu_file ) {
581 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading tab for menu highlight only
582 $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
583 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
584 $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : '';
585
586 if ( 'vigilante' !== $page || empty( $tab ) ) {
587 return $submenu_file;
588 }
589
590 $tab_to_submenu = array(
591 'activity-log' => 'vigilante-activity-log',
592 'file-integrity' => 'vigilante-file-integrity',
593 );
594
595 if ( isset( $tab_to_submenu[ $tab ] ) ) {
596 return $tab_to_submenu[ $tab ];
597 }
598
599 return $submenu_file;
600 }
601
602 /**
603 * Set browser tab title to show plugin name and active tab
604 *
605 * Changes "Dashboard ‹ Site Name — WordPress" to
606 * "Vigilant > Dashboard ‹ Site Name — WordPress"
607 *
608 * @param string $admin_title Full admin title.
609 * @param string $title Page title from add_menu_page/add_submenu_page.
610 * @return string Modified title.
611 */
612 public function set_admin_page_title( $admin_title, $title ) {
613 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading page slug for title only
614 $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
615
616 // Only modify on Vigilante pages
617 if ( 0 !== strpos( $page, 'vigilante' ) ) {
618 return $admin_title;
619 }
620
621 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
622 $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'dashboard';
623
624 if ( isset( $this->tabs[ $tab ] ) ) {
625 $tab_label = $this->tabs[ $tab ];
626 } else {
627 $tab_label = __( 'Dashboard', 'vigilante' );
628 }
629
630 $plugin_title = __( 'Vigilant', 'vigilante' ) . ' &rsaquo; ' . $tab_label;
631
632 // Replace the original page title portion
633 return str_replace( $title, $plugin_title, $admin_title );
634 }
635
636 /**
637 * Get count of users pending approval
638 *
639 * @return int Count of pending users.
640 */
641 private function get_pending_approvals_count() {
642 // Prevent early execution before WordPress is ready
643 if ( ! did_action( 'plugins_loaded' ) ) {
644 return 0;
645 }
646
647 $registration_approval = $this->settings->get_section( 'user_security' );
648 $approval_settings = $registration_approval['registration_approval'] ?? array();
649
650 if ( empty( $approval_settings['enabled'] ) ) {
651 return 0;
652 }
653
654 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Limited results in admin context.
655 $pending_users = get_users( array(
656 'meta_key' => 'vigilante_pending_approval',
657 'meta_value' => '1',
658 'fields' => 'ID',
659 ) );
660 // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
661
662 return count( $pending_users );
663 }
664
665 /**
666 * Calculate comprehensive security score (0-100)
667 *
668 * @param array $options Plugin options.
669 * @return int Security score.
670 */
671 private function calculate_security_score( $options ) {
672 $score = 0;
673 $max_score = 0;
674
675 // Module scores (60 points total)
676 $module_weights = array(
677 'firewall' => 10,
678 'security_headers' => 8,
679 'login_security' => 10,
680 'rest_api_security' => 6,
681 'user_security' => 8,
682 'wp_hardening' => 8,
683 'file_integrity' => 5,
684 'activity_log' => 5,
685 );
686
687 foreach ( $module_weights as $module => $weight ) {
688 $max_score += $weight;
689 if ( ! empty( $options['modules'][ $module ] ) ) {
690 $score += $weight;
691 }
692 }
693
694 // Firewall details (10 points)
695 if ( ! empty( $options['modules']['firewall'] ) ) {
696 $firewall = $options['firewall'] ?? array();
697 $max_score += 10;
698
699 $firewall_checks = array(
700 'block_sql_injection',
701 'block_xss_attacks',
702 'block_bad_query_strings',
703 'block_file_inclusion',
704 'block_directory_traversal',
705 );
706
707 $firewall_enabled = 0;
708 foreach ( $firewall_checks as $check ) {
709 if ( ! empty( $firewall[ $check ] ) ) {
710 $firewall_enabled++;
711 }
712 }
713 $score += min( 10, $firewall_enabled * 2 );
714 }
715
716 // Login security details (10 points)
717 if ( ! empty( $options['modules']['login_security'] ) ) {
718 $login = $options['login_security'] ?? array();
719 $max_score += 10;
720
721 // Max attempts configured
722 if ( isset( $login['max_attempts'] ) && $login['max_attempts'] <= 5 ) {
723 $score += 3;
724 }
725 // XML-RPC disabled
726 if ( ! empty( $login['disable_xmlrpc'] ) ) {
727 $score += 3;
728 }
729 // 2FA enabled
730 if ( ! empty( $login['two_factor']['enabled'] ) ) {
731 $score += 4;
732 }
733 }
734
735 // Security headers details (10 points)
736 if ( ! empty( $options['modules']['security_headers'] ) ) {
737 $headers = $options['security_headers'] ?? array();
738 $max_score += 10;
739
740 if ( ! empty( $headers['x_frame_options'] ) ) {
741 $score += 2;
742 }
743 if ( ! empty( $headers['x_content_type_options'] ) ) {
744 $score += 2;
745 }
746 if ( ! empty( $headers['hsts']['enabled'] ) ) {
747 $score += 3;
748 }
749 if ( ! empty( $headers['csp']['enabled'] ) ) {
750 $score += 3;
751 }
752 }
753
754 // User security details (10 points)
755 if ( ! empty( $options['modules']['user_security'] ) ) {
756 $user = $options['user_security'] ?? array();
757 $max_score += 10;
758
759 if ( ! empty( $user['block_insecure_usernames'] ) ) {
760 $score += 3;
761 }
762 if ( ! empty( $user['force_strong_passwords'] ) ) {
763 $score += 3;
764 }
765 if ( ! empty( $user['password_expiration']['enabled'] ) ) {
766 $score += 2;
767 }
768 if ( ! empty( $user['email_verification']['enabled'] ) ) {
769 $score += 2;
770 }
771 }
772
773 // Audit alerts details (6 points) - only when Security Audit is on,
774 // because the alerting layer rides on top of the activity log. Leaving
775 // both alert legs off keeps these points unearned.
776 if ( ! empty( $options['modules']['activity_log'] ) ) {
777 $alerts = isset( $options['audit_alerts'] ) ? (array) $options['audit_alerts'] : array();
778 $max_score += 6;
779 if ( Vigilante_Audit_Alerts::immediate_is_active( $alerts ) ) {
780 $score += 3;
781 }
782 if ( Vigilante_Audit_Alerts::threshold_is_active( $alerts ) ) {
783 $score += 3;
784 }
785 }
786
787 // Environment checks (8 points) - penalize insecure server configuration
788 $max_score += 8;
789 $env_score = 8;
790
791 // WP_DEBUG active in production is a security risk (exposes paths, errors)
792 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
793 $env_score -= 5;
794 }
795
796 // Accounts with insecure usernames (targeted by brute force attacks)
797 $insecure_admins = $this->get_insecure_admin_usernames();
798 if ( ! empty( $insecure_admins ) ) {
799 $env_score -= 3;
800 }
801
802 $score += max( 0, $env_score );
803
804 return $max_score > 0 ? round( ( $score / $max_score ) * 100 ) : 0;
805 }
806
807 /**
808 * Get security recommendations based on current settings
809 *
810 * @param array $options Plugin options.
811 * @return array Array of recommendations.
812 */
813 private function get_security_recommendations( $options ) {
814 $recommendations = array();
815
816 // Critical: Firewall disabled
817 if ( empty( $options['modules']['firewall'] ) ) {
818 $recommendations[] = array(
819 'icon' => 'warning',
820 'priority' => 'critical',
821 'message' => __( 'Enable Firewall to protect against common attacks.', 'vigilante' ),
822 );
823 }
824
825 // Critical: Login security disabled
826 if ( empty( $options['modules']['login_security'] ) ) {
827 $recommendations[] = array(
828 'icon' => 'warning',
829 'priority' => 'critical',
830 'message' => __( 'Enable Login Security to prevent brute force attacks.', 'vigilante' ),
831 );
832 }
833
834 // High: Security headers disabled
835 if ( empty( $options['modules']['security_headers'] ) ) {
836 $recommendations[] = array(
837 'icon' => 'admin-generic',
838 'priority' => 'high',
839 'message' => __( 'Enable Security Headers to protect against clickjacking and XSS.', 'vigilante' ),
840 );
841 }
842
843 // High: User security disabled
844 if ( empty( $options['modules']['user_security'] ) ) {
845 $recommendations[] = array(
846 'icon' => 'admin-users',
847 'priority' => 'high',
848 'message' => __( 'Enable User Security to enforce password policies and username protection.', 'vigilante' ),
849 );
850 }
851
852 // High: 2FA not enabled (only if login security is active)
853 $login = $options['login_security'] ?? array();
854 if ( ! empty( $options['modules']['login_security'] ) && empty( $login['two_factor']['enabled'] ) ) {
855 $recommendations[] = array(
856 'icon' => 'shield',
857 'priority' => 'high',
858 'message' => __( 'Enable Two-Factor Authentication for enhanced login security.', 'vigilante' ),
859 'tab' => 'login',
860 );
861 }
862
863 // Medium: REST API security disabled
864 if ( empty( $options['modules']['rest_api_security'] ) ) {
865 $recommendations[] = array(
866 'icon' => 'rest-api',
867 'priority' => 'medium',
868 'message' => __( 'Enable REST API Security to control API access and prevent enumeration.', 'vigilante' ),
869 );
870 }
871
872 // Medium: WP Hardening disabled
873 if ( empty( $options['modules']['wp_hardening'] ) ) {
874 $recommendations[] = array(
875 'icon' => 'lock',
876 'priority' => 'medium',
877 'message' => __( 'Enable WP Hardening to remove version info and protect core files.', 'vigilante' ),
878 );
879 }
880
881 // Medium: File integrity disabled
882 if ( empty( $options['modules']['file_integrity'] ) ) {
883 $recommendations[] = array(
884 'icon' => 'media-text',
885 'priority' => 'medium',
886 'message' => __( 'Enable File Integrity to detect unauthorized file changes.', 'vigilante' ),
887 );
888 }
889
890 // Medium: Security Audit disabled
891 if ( empty( $options['modules']['activity_log'] ) ) {
892 $recommendations[] = array(
893 'icon' => 'list-view',
894 'priority' => 'medium',
895 'message' => __( 'Enable Security Audit to track security events.', 'vigilante' ),
896 );
897 }
898
899 // Low: XML-RPC enabled (only if login security is active)
900 if ( ! empty( $options['modules']['login_security'] ) && empty( $login['disable_xmlrpc'] ) ) {
901 $recommendations[] = array(
902 'icon' => 'info',
903 'priority' => 'low',
904 'message' => __( 'Disable XML-RPC if not needed (reduces attack surface).', 'vigilante' ),
905 'tab' => 'login',
906 );
907 }
908
909 // Low: Strong passwords not enforced (only if user security is active)
910 $user = $options['user_security'] ?? array();
911 if ( ! empty( $options['modules']['user_security'] ) && empty( $user['force_strong_passwords'] ) ) {
912 $recommendations[] = array(
913 'icon' => 'admin-users',
914 'priority' => 'low',
915 'message' => __( 'Enforce strong passwords for all users.', 'vigilante' ),
916 'tab' => 'users',
917 );
918 }
919
920 // High: WP_DEBUG active in production (regardless of Vigilante settings)
921 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
922 $recommendations[] = array(
923 'icon' => 'warning',
924 'priority' => 'high',
925 'message' => __( 'WP_DEBUG is active. Debug mode exposes sensitive information and should be disabled in production.', 'vigilante' ),
926 'tab' => 'wp-hardening',
927 );
928 }
929
930 // Low: Users with display name matching login username (only if user security active)
931 if ( ! empty( $options['modules']['user_security'] ) ) {
932 $exposed_users = $this->get_users_with_exposed_login();
933 if ( ! empty( $exposed_users ) ) {
934 $recommendations[] = array(
935 'icon' => 'admin-users',
936 'priority' => 'low',
937 'message' => sprintf(
938 /* translators: %s: Comma-separated list of usernames */
939 __( 'These users have their login username as display name (publicly visible): %s', 'vigilante' ),
940 implode( ', ', $exposed_users )
941 ),
942 'tab' => 'users',
943 );
944 }
945 }
946
947 // High: Accounts with insecure usernames (regardless of module status)
948 $insecure_admins = $this->get_insecure_admin_usernames();
949 if ( ! empty( $insecure_admins ) ) {
950 $recommendations[] = array(
951 'icon' => 'warning',
952 'priority' => 'high',
953 'message' => sprintf(
954 /* translators: %s: Comma-separated list of usernames */
955 __( 'Insecure usernames detected: %s. These are commonly targeted in brute force attacks. Create new accounts with unique usernames and remove these.', 'vigilante' ),
956 implode( ', ', $insecure_admins )
957 ),
958 );
959 }
960
961 // Critical: Closed or removed plugins detected by the daily check.
962 // Reads from the cached state map populated by Vigilante_Plugin_Status, so
963 // there is no extra HTTP call here. Ignored slugs are filtered out so the
964 // recommendation respects the user's per-slug Ignore decisions.
965 if ( ! empty( $options['modules']['file_integrity'] ) && ! empty( $options['file_integrity']['check_closed_plugins'] ) ) {
966 if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) {
967 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
968 }
969 $closed_checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
970 $closed_active = $closed_checker->get_closed_plugins();
971 if ( ! empty( $closed_active ) ) {
972 $names = array();
973 foreach ( $closed_active as $slug => $entry ) {
974 $names[] = isset( $entry['name'] ) ? $entry['name'] : $slug;
975 }
976 $recommendations[] = array(
977 'icon' => 'warning',
978 'priority' => 'critical',
979 'message' => sprintf(
980 /* translators: 1: count, 2: comma-separated plugin names */
981 _n(
982 '%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.',
983 '%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.',
984 count( $closed_active ),
985 'vigilante'
986 ),
987 count( $closed_active ),
988 implode( ', ', $names )
989 ),
990 'tab' => 'file-integrity',
991 );
992 }
993 }
994
995 // Medium: Security Audit is on but no audit alert is configured. The
996 // alerting layer only makes sense while the activity log is running.
997 if ( ! empty( $options['modules']['activity_log'] ) ) {
998 $alerts = isset( $options['audit_alerts'] ) ? (array) $options['audit_alerts'] : array();
999 if ( ! Vigilante_Audit_Alerts::has_active_alerts( $alerts ) ) {
1000 $recommendations[] = array(
1001 'icon' => 'email-alt',
1002 'priority' => 'medium',
1003 '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' ),
1004 'tab' => 'activity-log',
1005 );
1006 }
1007 }
1008
1009 // Sort by priority
1010 $priority_order = array( 'critical' => 0, 'high' => 1, 'medium' => 2, 'low' => 3 );
1011 usort( $recommendations, function( $a, $b ) use ( $priority_order ) {
1012 return ( $priority_order[ $a['priority'] ] ?? 99 ) - ( $priority_order[ $b['priority'] ] ?? 99 );
1013 } );
1014
1015 return $recommendations;
1016 }
1017
1018 /**
1019 * Get users whose display name matches their login username
1020 *
1021 * Limited to administrators and editors for performance and relevance.
1022 * Cached with transient to avoid repeated queries on every dashboard load.
1023 *
1024 * @return array Array of usernames with exposed login.
1025 */
1026 private function get_users_with_exposed_login() {
1027 $cache_key = 'vigilante_exposed_display_names';
1028 $cached = get_transient( $cache_key );
1029
1030 if ( false !== $cached ) {
1031 return $cached;
1032 }
1033
1034 $exposed = array();
1035 $users = get_users( array(
1036 'role__in' => array( 'administrator', 'editor' ),
1037 'fields' => array( 'ID', 'user_login', 'display_name' ),
1038 ) );
1039
1040 foreach ( $users as $user ) {
1041 if ( strcasecmp( $user->display_name, $user->user_login ) === 0 ) {
1042 $exposed[] = $user->user_login;
1043 }
1044 }
1045
1046 // Cache for 12 hours
1047 set_transient( $cache_key, $exposed, 12 * HOUR_IN_SECONDS );
1048
1049 return $exposed;
1050 }
1051
1052 /**
1053 * Get accounts with insecure usernames
1054 *
1055 * Checks for common default usernames that are targeted by brute force attacks.
1056 * Detects any user regardless of role (consistent with username creation blocking).
1057 * Uses WordPress object cache via get_user_by() so no transient needed.
1058 *
1059 * @return array Array of insecure usernames found.
1060 */
1061 private function get_insecure_admin_usernames() {
1062 $priority_usernames = array( 'admin', 'administrator', 'root', 'test', 'user', 'guest', 'info', 'sysadmin', 'webmaster' );
1063 $found = array();
1064
1065 foreach ( $priority_usernames as $username ) {
1066 $user = get_user_by( 'login', $username );
1067 if ( $user ) {
1068 $found[] = $username;
1069 }
1070 }
1071
1072 return $found;
1073 }
1074
1075 /**
1076 * Get security status for menu badge
1077 *
1078 * Returns count of disabled modules and whether there are critical issues.
1079 * Critical = Firewall or Login Security disabled.
1080 *
1081 * @return array Array with 'count' and 'has_critical'.
1082 */
1083 public function get_security_status_for_badge() {
1084 $options = $this->settings->get_all_options();
1085 $modules = $options['modules'] ?? array();
1086
1087 // Count disabled modules
1088 $disabled_count = 0;
1089 $has_critical = false;
1090
1091 // Critical modules - if disabled, badge is red
1092 $critical_modules = array( 'firewall', 'login_security' );
1093
1094 foreach ( $modules as $module => $enabled ) {
1095 // Handle both boolean and string values ('1', '0', true, false)
1096 $is_enabled = filter_var( $enabled, FILTER_VALIDATE_BOOLEAN );
1097
1098 if ( ! $is_enabled ) {
1099 $disabled_count++;
1100
1101 // Check if this is a critical module
1102 if ( in_array( $module, $critical_modules, true ) ) {
1103 $has_critical = true;
1104 }
1105 }
1106 }
1107
1108 return array(
1109 'count' => $disabled_count,
1110 'has_critical' => $has_critical,
1111 );
1112 }
1113
1114 /**
1115 * Get count of security issues for menu badge (deprecated, use get_security_status_for_badge)
1116 *
1117 * @return int Count of critical/high issues.
1118 */
1119 public function get_security_issues_count() {
1120 $status = $this->get_security_status_for_badge();
1121 return $status['count'];
1122 }
1123
1124 /**
1125 * Register settings
1126 */
1127 public function register_settings() {
1128 register_setting(
1129 'vigilante_options',
1130 Vigilante_Settings::OPTION_NAME,
1131 array( $this->settings, 'validate_options' )
1132 );
1133 }
1134
1135 /**
1136 * Index behind the settings search box.
1137 *
1138 * Each entry points at one settings row. The search matches on the label,
1139 * on its English original and on 'keywords', which are extra terms someone
1140 * might type instead of the label itself.
1141 *
1142 * Those keywords are wrapped in _x() with the context "settings search
1143 * keywords" so every locale can supply its own: the source strings are in
1144 * English, and a Spanish user typing "contrasena" or a German one typing
1145 * "Kennwort" only reaches the password settings if that locale translated
1146 * them. Translators can add, drop or replace terms freely, one per space;
1147 * they are never displayed, only matched against what the user types.
1148 *
1149 * The list is maintained by hand, so a new settings row needs an entry here
1150 * or it cannot be found. It had drifted to 68 of 131 rows before 2.9.7.
1151 *
1152 * @return array
1153 */
1154 private function get_search_index() {
1155 return array(
1156 // Firewall - Main
1157 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' => _x( 'block bad bots blocking blocked deny malicious harmful bot crawler crawlers spider scraper robots', 'settings search keywords', 'vigilante' ) ),
1158 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' => _x( 'block malicious requests blocking blocked deny attack attacks exploit injection sqli xss rfi lfi request traffic', 'settings search keywords', 'vigilante' ) ),
1159 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' => _x( 'rate limiting throttle flood burst limit limits', 'settings search keywords', 'vigilante' ) ),
1160 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' => _x( 'brute force protection bruteforce login', 'settings search keywords', 'vigilante' ) ),
1161 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' => _x( 'ip whitelist ips address addresses cidr ipv4 ipv6 allowlist allowed trusted', 'settings search keywords', 'vigilante' ) ),
1162 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' => _x( 'ip blacklist ips address addresses cidr ipv4 ipv6 blocklist denylist banned', 'settings search keywords', 'vigilante' ) ),
1163 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' => _x( 'user-agent whitelist ua useragent browser allowlist allowed trusted user', 'settings search keywords', 'vigilante' ) ),
1164 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' => _x( 'user-agent blacklist ua useragent browser blocklist denylist banned user', 'settings search keywords', 'vigilante' ) ),
1165 // Firewall - Server Protection
1166 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' => _x( 'directory browsing folder folders listing indexing index', 'settings search keywords', 'vigilante' ) ),
1167 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' => _x( 'protect wp-config php protection secure lock', 'settings search keywords', 'vigilante' ) ),
1168 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' => _x( 'protect wp-cron php protection secure lock cron scheduled tasks block spam', 'settings search keywords', 'vigilante' ) ),
1169 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' => _x( 'protect wp-includes protection secure lock', 'settings search keywords', 'vigilante' ) ),
1170 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' => _x( 'php in uploads media upload', 'settings search keywords', 'vigilante' ) ),
1171 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' => _x( 'sensitive files private secret file log', 'settings search keywords', 'vigilante' ) ),
1172 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' => _x( 'limit http methods', 'settings search keywords', 'vigilante' ) ),
1173 // Security Headers
1174 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' => _x( 'x-frame-options headers', 'settings search keywords', 'vigilante' ) ),
1175 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' => _x( 'x-content-type-options content headers', 'settings search keywords', 'vigilante' ) ),
1176 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' => _x( 'referrer-policy headers', 'settings search keywords', 'vigilante' ) ),
1177 array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'HSTS', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'HSTS', 'vigilante' ), 'label_en' => 'HSTS', 'keywords' => _x( 'hsts strict transport security ssl tls https headers', 'settings search keywords', 'vigilante' ) ),
1178 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' => _x( 'content security policy csp xss headers', 'settings search keywords', 'vigilante' ) ),
1179 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' => _x( 'server signature fingerprint banner', 'settings search keywords', 'vigilante' ) ),
1180 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' => _x( 'remove fingerprinting headers fingerprint banner header http', 'settings search keywords', 'vigilante' ) ),
1181 // Login Security
1182 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' => _x( 'custom login url signin log-in access slug', 'settings search keywords', 'vigilante' ) ),
1183 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' => _x( 'two-factor authentication 2fa mfa otp totp authenticator', 'settings search keywords', 'vigilante' ) ),
1184 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( '2FA', 'vigilante' ), 'label_en' => '2FA', 'keywords' => _x( '2fa two-factor mfa otp totp authenticator', 'settings search keywords', 'vigilante' ) ),
1185 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' => _x( 'failed login attempts signin log-in access tries retries', 'settings search keywords', 'vigilante' ) ),
1186 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Lockout', 'vigilante' ), 'label_en' => 'Lockout', 'keywords' => _x( 'lockout lock ban block login', 'settings search keywords', 'vigilante' ) ),
1187 // REST API
1188 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' => _x( 'access mode rest api', 'settings search keywords', 'vigilante' ) ),
1189 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' => _x( 'block user enumeration blocking blocked deny users account author slug', 'settings search keywords', 'vigilante' ) ),
1190 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' => _x( 'disable jsonp', 'settings search keywords', 'vigilante' ) ),
1191 // User Security
1192 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' => _x( 'username protection admin', 'settings search keywords', 'vigilante' ) ),
1193 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' => _x( 'password strength passwords credentials', 'settings search keywords', 'vigilante' ) ),
1194 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' => _x( 'admin monitoring administrator administrators', 'settings search keywords', 'vigilante' ) ),
1195 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' => _x( 'registration approval signup register approve moderate', 'settings search keywords', 'vigilante' ) ),
1196 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' => _x( 'session limits sessions concurrent', 'settings search keywords', 'vigilante' ) ),
1197 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' => _x( 'password expiration passwords credentials expiry expire caducity', 'settings search keywords', 'vigilante' ) ),
1198 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' => _x( 'email verification mail notification notify verify confirm', 'settings search keywords', 'vigilante' ) ),
1199 // WP Hardening
1200 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' => _x( 'database hardening db mysql tables', 'settings search keywords', 'vigilante' ) ),
1201 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' => _x( 'database prefix db mysql tables table', 'settings search keywords', 'vigilante' ) ),
1202 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' => _x( 'disable file editing files editor edit', 'settings search keywords', 'vigilante' ) ),
1203 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' => _x( 'disable plugin theme installation install', 'settings search keywords', 'vigilante' ) ),
1204 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' => _x( 'force ssl admin bruteforce administrator administrators tls https', 'settings search keywords', 'vigilante' ) ),
1205 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' => _x( 'disable wp cron scheduled tasks wp-cron', 'settings search keywords', 'vigilante' ) ),
1206 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' => _x( 'comment security comments spam honeypot url', 'settings search keywords', 'vigilante' ) ),
1207 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' => _x( 'header cleanup headers http meta generator rsd wlwmanifest', 'settings search keywords', 'vigilante' ) ),
1208 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' => _x( 'remove wordpress version generator meta', 'settings search keywords', 'vigilante' ) ),
1209 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' => _x( 'remove version from assets', 'settings search keywords', 'vigilante' ) ),
1210 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-xmlrpc', 'label' => __( 'Disable XML-RPC', 'vigilante' ), 'label_en' => 'Disable XML-RPC', 'keywords' => _x( 'disable xml-rpc xmlrpc rpc remote jetpack app pingback trackback', 'settings search keywords', 'vigilante' ) ),
1211 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' => _x( 'rss feed settings feeds atom', 'settings search keywords', 'vigilante' ) ),
1212 // File Integrity
1213 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' => _x( 'file integrity monitoring files checksum checksums tamper', 'settings search keywords', 'vigilante' ) ),
1214 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' => _x( 'scan schedule scans scanning check cron', 'settings search keywords', 'vigilante' ) ),
1215 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' => _x( 'instant alert alerts notification warning email', 'settings search keywords', 'vigilante' ) ),
1216 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' => _x( 'ignored files file exclude', 'settings search keywords', 'vigilante' ) ),
1217 // Security Audit
1218 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' => _x( 'retention keep days storage log', 'settings search keywords', 'vigilante' ) ),
1219 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' => _x( 'events to log', 'settings search keywords', 'vigilante' ) ),
1220 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' => _x( 'option tracking', 'settings search keywords', 'vigilante' ) ),
1221 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' => _x( 'exclusions roles ip', 'settings search keywords', 'vigilante' ) ),
1222 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' => _x( 'audit alerts email mail warning critical', 'settings search keywords', 'vigilante' ) ),
1223 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' => _x( 'immediate alerts email mail critical warning', 'settings search keywords', 'vigilante' ) ),
1224 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' => _x( 'threshold alerts email mail login', 'settings search keywords', 'vigilante' ) ),
1225 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' => _x( 'recent activity log', 'settings search keywords', 'vigilante' ) ),
1226 // Settings & Tools
1227 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' => _x( 'notification settings email', 'settings search keywords', 'vigilante' ) ),
1228 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' => _x( 'additional recipients email recipient', 'settings search keywords', 'vigilante' ) ),
1229 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' => _x( 'export settings json', 'settings search keywords', 'vigilante' ) ),
1230 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' => _x( 'import settings json', 'settings search keywords', 'vigilante' ) ),
1231 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' => _x( 'reset to defaults', 'settings search keywords', 'vigilante' ) ),
1232 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' => _x( 'create backup', 'settings search keywords', 'vigilante' ) ),
1233 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' => _x( 'database backup db mysql tables', 'settings search keywords', 'vigilante' ) ),
1234 // Entradas anadidas en la 2.9.7 tras comprobar que el indice cubria 68 de
1235 // las 131 filas de ajustes: buscar XML-RPC, por ejemplo, no devolvia nada.
1236 // El indice se mantiene a mano, asi que al anadir una fila de ajustes hay
1237 // que anadirla tambien aqui.
1238 array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Notification settings', 'vigilante' ), 'anchor' => 'vigilante-section-tools-notifications', 'label' => __( 'WordPress Admin Email', 'vigilante' ), 'label_en' => 'WordPress Admin Email', 'keywords' => _x( 'wordpress admin email administrator administrators mail notification notify', 'settings search keywords', 'vigilante' ) ),
1239 array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Notification settings', 'vigilante' ), 'anchor' => 'vigilante-section-tools-notifications', 'label' => __( 'Plugin Deactivation', 'vigilante' ), 'label_en' => 'Plugin Deactivation', 'keywords' => _x( 'plugin deactivation', 'settings search keywords', 'vigilante' ) ),
1240 array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Block Bad Query Strings', 'vigilante' ), 'label_en' => 'Block Bad Query Strings', 'keywords' => _x( 'block bad query strings blocking blocked deny malicious harmful', 'settings search keywords', 'vigilante' ) ),
1241 array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'SQL Injection Protection', 'vigilante' ), 'label_en' => 'SQL Injection Protection', 'keywords' => _x( 'sql injection protection', 'settings search keywords', 'vigilante' ) ),
1242 array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'XSS Protection', 'vigilante' ), 'label_en' => 'XSS Protection', 'keywords' => _x( 'xss protection', 'settings search keywords', 'vigilante' ) ),
1243 array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'File Inclusion Protection', 'vigilante' ), 'label_en' => 'File Inclusion Protection', 'keywords' => _x( 'file inclusion protection files', 'settings search keywords', 'vigilante' ) ),
1244 array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Directory Traversal Protection', 'vigilante' ), 'label_en' => 'Directory Traversal Protection', 'keywords' => _x( 'directory traversal protection folder folders', 'settings search keywords', 'vigilante' ) ),
1245 array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Enable Rate Limiting', 'vigilante' ), 'label_en' => 'Enable Rate Limiting', 'keywords' => _x( 'enable rate limiting throttle flood burst limit limits', 'settings search keywords', 'vigilante' ) ),
1246 array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Requests per Minute', 'vigilante' ), 'label_en' => 'Requests per Minute', 'keywords' => _x( 'requests per minute request traffic', 'settings search keywords', 'vigilante' ) ),
1247 array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Block Duration (seconds)', 'vigilante' ), 'label_en' => 'Block Duration (seconds)', 'keywords' => _x( 'block duration seconds blocking blocked deny', 'settings search keywords', 'vigilante' ) ),
1248 array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Progressive Blocking', 'vigilante' ), 'label_en' => 'Progressive Blocking', 'keywords' => _x( 'progressive blocking', 'settings search keywords', 'vigilante' ) ),
1249 array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Maximum Block Duration', 'vigilante' ), 'label_en' => 'Maximum Block Duration', 'keywords' => _x( 'maximum block duration blocking blocked deny', 'settings search keywords', 'vigilante' ) ),
1250 array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Visitor IP detection', 'vigilante' ), 'label_en' => 'Visitor IP detection', 'keywords' => _x( 'visitor ip detection ips address addresses cidr ipv4 ipv6', 'settings search keywords', 'vigilante' ) ),
1251 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Max Login Attempts', 'vigilante' ), 'label_en' => 'Max Login Attempts', 'keywords' => _x( 'max login attempts signin log-in access tries retries', 'settings search keywords', 'vigilante' ) ),
1252 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Lockout Duration', 'vigilante' ), 'label_en' => 'Lockout Duration', 'keywords' => _x( 'lockout duration lock ban block', 'settings search keywords', 'vigilante' ) ),
1253 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Progressive Lockout', 'vigilante' ), 'label_en' => 'Progressive Lockout', 'keywords' => _x( 'progressive lockout lock ban block', 'settings search keywords', 'vigilante' ) ),
1254 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Hide Login Errors', 'vigilante' ), 'label_en' => 'Hide Login Errors', 'keywords' => _x( 'hide login errors signin log-in access error debug log', 'settings search keywords', 'vigilante' ) ),
1255 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Disable Application Passwords', 'vigilante' ), 'label_en' => 'Disable Application Passwords', 'keywords' => _x( 'disable application passwords password credentials', 'settings search keywords', 'vigilante' ) ),
1256 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Login URL Slug', 'vigilante' ), 'label_en' => 'Login URL Slug', 'keywords' => _x( 'login url slug signin log-in access path', 'settings search keywords', 'vigilante' ) ),
1257 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Notify users', 'vigilante' ), 'label_en' => 'Notify users', 'keywords' => _x( 'notify users notification alert email user accounts', 'settings search keywords', 'vigilante' ) ),
1258 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Notify on Lockout', 'vigilante' ), 'label_en' => 'Notify on Lockout', 'keywords' => _x( 'notify on lockout notification alert email lock ban block', 'settings search keywords', 'vigilante' ) ),
1259 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Notify on Admin Login', 'vigilante' ), 'label_en' => 'Notify on Admin Login', 'keywords' => _x( 'notify on admin login notification alert email administrator administrators signin log-in access', 'settings search keywords', 'vigilante' ) ),
1260 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Current settings', 'vigilante' ), 'label_en' => 'Current settings', 'keywords' => _x( 'current settings', 'settings search keywords', 'vigilante' ) ),
1261 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Blocked IPs', 'vigilante' ), 'label_en' => 'Blocked IPs', 'keywords' => _x( 'blocked ips', 'settings search keywords', 'vigilante' ) ),
1262 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Enable 2FA', 'vigilante' ), 'label_en' => 'Enable 2FA', 'keywords' => _x( 'enable 2fa two-factor mfa otp totp authenticator', 'settings search keywords', 'vigilante' ) ),
1263 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Verification method', 'vigilante' ), 'label_en' => 'Verification method', 'keywords' => _x( 'verification method verify confirm', 'settings search keywords', 'vigilante' ) ),
1264 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Enforce for roles', 'vigilante' ), 'label_en' => 'Enforce for roles', 'keywords' => _x( 'enforce for roles role capabilities', 'settings search keywords', 'vigilante' ) ),
1265 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Exclude specific users', 'vigilante' ), 'label_en' => 'Exclude specific users', 'keywords' => _x( 'exclude specific users user accounts', 'settings search keywords', 'vigilante' ) ),
1266 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Remember device', 'vigilante' ), 'label_en' => 'Remember device', 'keywords' => _x( 'remember device', 'settings search keywords', 'vigilante' ) ),
1267 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Grace period', 'vigilante' ), 'label_en' => 'Grace period', 'keywords' => _x( 'grace period', 'settings search keywords', 'vigilante' ) ),
1268 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Email sender name', 'vigilante' ), 'label_en' => 'Email sender name', 'keywords' => _x( 'email sender name mail notification notify names', 'settings search keywords', 'vigilante' ) ),
1269 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Reset user TOTP', 'vigilante' ), 'label_en' => 'Reset user TOTP', 'keywords' => _x( 'reset user totp users account 2fa authenticator app', 'settings search keywords', 'vigilante' ) ),
1270 array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Notify on enable', 'vigilante' ), 'label_en' => 'Notify on enable', 'keywords' => _x( 'notify on enable notification alert email', 'settings search keywords', 'vigilante' ) ),
1271 array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Enable CSP', 'vigilante' ), 'label_en' => 'Enable CSP', 'keywords' => _x( 'enable csp content security policy', 'settings search keywords', 'vigilante' ) ),
1272 array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Report Only Mode', 'vigilante' ), 'label_en' => 'Report Only Mode', 'keywords' => _x( 'report only mode', 'settings search keywords', 'vigilante' ) ),
1273 array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Redirect HTTP to HTTPS', 'vigilante' ), 'label_en' => 'Redirect HTTP to HTTPS', 'keywords' => _x( 'redirect http to https redirection forward ssl tls secure', 'settings search keywords', 'vigilante' ) ),
1274 array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Fix Mixed Content', 'vigilante' ), 'label_en' => 'Fix Mixed Content', 'keywords' => _x( 'fix mixed content insecure http', 'settings search keywords', 'vigilante' ) ),
1275 array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'field-upgrade-insecure-requests', 'label' => __( 'Upgrade Insecure Requests', 'vigilante' ), 'label_en' => 'Upgrade Insecure Requests', 'keywords' => _x( 'upgrade insecure requests mixed content csp https external resources', 'settings search keywords', 'vigilante' ) ),
1276 array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Rewrite Site Address on Activation', 'vigilante' ), 'label_en' => 'Rewrite Site Address on Activation', 'keywords' => _x( 'rewrite site address on activation', 'settings search keywords', 'vigilante' ) ),
1277 array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Enable HSTS', 'vigilante' ), 'label_en' => 'Enable HSTS', 'keywords' => _x( 'enable hsts strict transport security', 'settings search keywords', 'vigilante' ) ),
1278 array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Max Age', 'vigilante' ), 'label_en' => 'Max Age', 'keywords' => _x( 'max age', 'settings search keywords', 'vigilante' ) ),
1279 array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Include Subdomains', 'vigilante' ), 'label_en' => 'Include Subdomains', 'keywords' => _x( 'include subdomains', 'settings search keywords', 'vigilante' ) ),
1280 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Block Insecure Usernames', 'vigilante' ), 'label_en' => 'Block Insecure Usernames', 'keywords' => _x( 'block insecure usernames blocking blocked deny weak unsafe', 'settings search keywords', 'vigilante' ) ),
1281 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Enforce Strong Passwords', 'vigilante' ), 'label_en' => 'Enforce Strong Passwords', 'keywords' => _x( 'enforce strong passwords complexity password credentials', 'settings search keywords', 'vigilante' ) ),
1282 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Minimum Password Length', 'vigilante' ), 'label_en' => 'Minimum Password Length', 'keywords' => _x( 'minimum password length passwords credentials characters', 'settings search keywords', 'vigilante' ) ),
1283 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Password Requirements', 'vigilante' ), 'label_en' => 'Password Requirements', 'keywords' => _x( 'password requirements passwords credentials', 'settings search keywords', 'vigilante' ) ),
1284 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Apply Password Rules To', 'vigilante' ), 'label_en' => 'Apply Password Rules To', 'keywords' => _x( 'apply password rules to passwords credentials', 'settings search keywords', 'vigilante' ) ),
1285 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Block Author Scanning', 'vigilante' ), 'label_en' => 'Block Author Scanning', 'keywords' => _x( 'block author scanning blocking blocked deny authors enumeration probing', 'settings search keywords', 'vigilante' ) ),
1286 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Display Name Protection', 'vigilante' ), 'label_en' => 'Display Name Protection', 'keywords' => _x( 'display name protection public visible names', 'settings search keywords', 'vigilante' ) ),
1287 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Admin monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-users-admin-monitoring', 'label' => __( 'New Administrator Alert', 'vigilante' ), 'label_en' => 'New Administrator Alert', 'keywords' => _x( 'new administrator alert alerts notification warning', 'settings search keywords', 'vigilante' ) ),
1288 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Admin monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-users-admin-monitoring', 'label' => __( 'Admin Email Change Alert', 'vigilante' ), 'label_en' => 'Admin Email Change Alert', 'keywords' => _x( 'admin email change alert administrator administrators mail notification notify alerts warning', 'settings search keywords', 'vigilante' ) ),
1289 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Admin monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-users-admin-monitoring', 'label' => __( 'Permission Elevation Alert', 'vigilante' ), 'label_en' => 'Permission Elevation Alert', 'keywords' => _x( 'permission elevation alert alerts notification warning', 'settings search keywords', 'vigilante' ) ),
1290 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Admin monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-users-admin-monitoring', 'label' => __( 'Admin Password Change Alert', 'vigilante' ), 'label_en' => 'Admin Password Change Alert', 'keywords' => _x( 'admin password change alert administrator administrators passwords credentials alerts notification warning', 'settings search keywords', 'vigilante' ) ),
1291 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Registration approval', 'vigilante' ), 'anchor' => 'vigilante-section-users-registration', 'label' => __( 'Enable Registration Approval', 'vigilante' ), 'label_en' => 'Enable Registration Approval', 'keywords' => _x( 'enable registration approval signup register approve moderate', 'settings search keywords', 'vigilante' ) ),
1292 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Registration approval', 'vigilante' ), 'anchor' => 'vigilante-section-users-registration', 'label' => __( 'Notify Admin', 'vigilante' ), 'label_en' => 'Notify Admin', 'keywords' => _x( 'notify admin notification alert email administrator administrators', 'settings search keywords', 'vigilante' ) ),
1293 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Registration approval', 'vigilante' ), 'anchor' => 'vigilante-section-users-registration', 'label' => __( 'Auto-reject After', 'vigilante' ), 'label_en' => 'Auto-reject After', 'keywords' => _x( 'auto-reject after', 'settings search keywords', 'vigilante' ) ),
1294 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Session limits', 'vigilante' ), 'anchor' => 'vigilante-section-users-sessions', 'label' => __( 'Enable Session Limits', 'vigilante' ), 'label_en' => 'Enable Session Limits', 'keywords' => _x( 'enable session limits sessions concurrent', 'settings search keywords', 'vigilante' ) ),
1295 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Session limits', 'vigilante' ), 'anchor' => 'vigilante-section-users-sessions', 'label' => __( 'Maximum Sessions', 'vigilante' ), 'label_en' => 'Maximum Sessions', 'keywords' => _x( 'maximum sessions session concurrent', 'settings search keywords', 'vigilante' ) ),
1296 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Session limits', 'vigilante' ), 'anchor' => 'vigilante-section-users-sessions', 'label' => __( 'When Limit Exceeded', 'vigilante' ), 'label_en' => 'When Limit Exceeded', 'keywords' => _x( 'when limit exceeded', 'settings search keywords', 'vigilante' ) ),
1297 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Session limits', 'vigilante' ), 'anchor' => 'vigilante-section-users-sessions', 'label' => __( 'Exclude Administrators', 'vigilante' ), 'label_en' => 'Exclude Administrators', 'keywords' => _x( 'exclude administrators', 'settings search keywords', 'vigilante' ) ),
1298 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Enable Password Expiration', 'vigilante' ), 'label_en' => 'Enable Password Expiration', 'keywords' => _x( 'enable password expiration passwords credentials expiry expire caducity', 'settings search keywords', 'vigilante' ) ),
1299 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Expire After', 'vigilante' ), 'label_en' => 'Expire After', 'keywords' => _x( 'expire after', 'settings search keywords', 'vigilante' ) ),
1300 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Warning Period', 'vigilante' ), 'label_en' => 'Warning Period', 'keywords' => _x( 'warning period', 'settings search keywords', 'vigilante' ) ),
1301 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Password History', 'vigilante' ), 'label_en' => 'Password History', 'keywords' => _x( 'password history passwords credentials reuse previous', 'settings search keywords', 'vigilante' ) ),
1302 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Email Reminder', 'vigilante' ), 'label_en' => 'Email Reminder', 'keywords' => _x( 'email reminder mail notification notify', 'settings search keywords', 'vigilante' ) ),
1303 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Affected Roles', 'vigilante' ), 'label_en' => 'Affected Roles', 'keywords' => _x( 'affected roles role capabilities', 'settings search keywords', 'vigilante' ) ),
1304 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Exclude specific users', 'vigilante' ), 'label_en' => 'Exclude specific users', 'keywords' => _x( 'exclude specific users user accounts', 'settings search keywords', 'vigilante' ) ),
1305 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Email verification', 'vigilante' ), 'anchor' => 'vigilante-section-users-email-verify', 'label' => __( 'Enable Email Verification', 'vigilante' ), 'label_en' => 'Enable Email Verification', 'keywords' => _x( 'enable email verification mail notification notify verify confirm', 'settings search keywords', 'vigilante' ) ),
1306 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Email verification', 'vigilante' ), 'anchor' => 'vigilante-section-users-email-verify', 'label' => __( 'Link Expiration', 'vigilante' ), 'label_en' => 'Link Expiration', 'keywords' => _x( 'link expiration expiry expire caducity', 'settings search keywords', 'vigilante' ) ),
1307 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Email verification', 'vigilante' ), 'anchor' => 'vigilante-section-users-email-verify', 'label' => __( 'Allow Resend', 'vigilante' ), 'label_en' => 'Allow Resend', 'keywords' => _x( 'allow resend', 'settings search keywords', 'vigilante' ) ),
1308 array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Email verification', 'vigilante' ), 'anchor' => 'vigilante-section-users-email-verify', 'label' => __( 'Auto-delete Unverified', 'vigilante' ), 'label_en' => 'Auto-delete Unverified', 'keywords' => _x( 'auto-delete unverified', 'settings search keywords', 'vigilante' ) ),
1309 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Database Hardening', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-database', 'label' => __( 'Current prefix', 'vigilante' ), 'label_en' => 'Current prefix', 'keywords' => _x( 'current prefix database db table tables mysql', 'settings search keywords', 'vigilante' ) ),
1310 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Database Hardening', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-database', 'label' => __( 'New prefix', 'vigilante' ), 'label_en' => 'New prefix', 'keywords' => _x( 'new prefix database db table tables mysql', 'settings search keywords', 'vigilante' ) ),
1311 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-wpconfig', 'label' => __( 'Disable File Editor', 'vigilante' ), 'label_en' => 'Disable File Editor', 'keywords' => _x( 'disable file editor files edit editing', 'settings search keywords', 'vigilante' ) ),
1312 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-wpconfig', 'label' => __( 'Disable File Modifications', 'vigilante' ), 'label_en' => 'Disable File Modifications', 'keywords' => _x( 'disable file modifications files modify install update', 'settings search keywords', 'vigilante' ) ),
1313 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-wpconfig', 'label' => __( 'Hide PHP errors from visitors', 'vigilante' ), 'label_en' => 'Hide PHP errors from visitors', 'keywords' => _x( 'hide php errors from visitors error debug log', 'settings search keywords', 'vigilante' ) ),
1314 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'XML-RPC', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-xmlrpc', 'label' => __( 'XML-RPC access', 'vigilante' ), 'label_en' => 'XML-RPC access', 'keywords' => _x( 'xml-rpc access xmlrpc rpc remote jetpack app', 'settings search keywords', 'vigilante' ) ),
1315 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Comment Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-comments', 'label' => __( 'Disable Pingbacks', 'vigilante' ), 'label_en' => 'Disable Pingbacks', 'keywords' => _x( 'disable pingbacks pingback ping', 'settings search keywords', 'vigilante' ) ),
1316 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Comment Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-comments', 'label' => __( 'Disable Trackbacks', 'vigilante' ), 'label_en' => 'Disable Trackbacks', 'keywords' => _x( 'disable trackbacks trackback ping', 'settings search keywords', 'vigilante' ) ),
1317 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Comment Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-comments', 'label' => __( 'Require Moderation', 'vigilante' ), 'label_en' => 'Require Moderation', 'keywords' => _x( 'require moderation moderate approve', 'settings search keywords', 'vigilante' ) ),
1318 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Comment Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-comments', 'label' => __( 'Close Old Comments', 'vigilante' ), 'label_en' => 'Close Old Comments', 'keywords' => _x( 'close old comments comment discussion', 'settings search keywords', 'vigilante' ) ),
1319 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Comment Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-comments', 'label' => __( 'Honeypot Protection', 'vigilante' ), 'label_en' => 'Honeypot Protection', 'keywords' => _x( 'honeypot protection spam bots trap', 'settings search keywords', 'vigilante' ) ),
1320 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Remove Generator', 'vigilante' ), 'label_en' => 'Remove Generator', 'keywords' => _x( 'remove generator version meta', 'settings search keywords', 'vigilante' ) ),
1321 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Remove RSD Link', 'vigilante' ), 'label_en' => 'Remove RSD Link', 'keywords' => _x( 'remove rsd link discovery', 'settings search keywords', 'vigilante' ) ),
1322 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Remove WLW Manifest', 'vigilante' ), 'label_en' => 'Remove WLW Manifest', 'keywords' => _x( 'remove wlw manifest wlwmanifest', 'settings search keywords', 'vigilante' ) ),
1323 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Remove Shortlink', 'vigilante' ), 'label_en' => 'Remove Shortlink', 'keywords' => _x( 'remove shortlink link', 'settings search keywords', 'vigilante' ) ),
1324 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Remove REST API Link', 'vigilante' ), 'label_en' => 'Remove REST API Link', 'keywords' => _x( 'remove rest api link json endpoint', 'settings search keywords', 'vigilante' ) ),
1325 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'RSS Feed Settings', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-rss', 'label' => __( 'Disable Feeds', 'vigilante' ), 'label_en' => 'Disable Feeds', 'keywords' => _x( 'disable feeds feed rss atom syndication', 'settings search keywords', 'vigilante' ) ),
1326 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'RSS Feed Settings', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-rss', 'label' => __( 'Disable If No Content', 'vigilante' ), 'label_en' => 'Disable If No Content', 'keywords' => _x( 'disable if no content', 'settings search keywords', 'vigilante' ) ),
1327 array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'RSS Feed Settings', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-rss', 'label' => __( 'Remove Feed Version', 'vigilante' ), 'label_en' => 'Remove Feed Version', 'keywords' => _x( 'remove feed version feeds rss atom', 'settings search keywords', 'vigilante' ) ),
1328 array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'vigilante-section-audit-alerts', 'label' => __( 'Alert on severity', 'vigilante' ), 'label_en' => 'Alert on severity', 'keywords' => _x( 'alert on severity alerts notification warning level critical', 'settings search keywords', 'vigilante' ) ),
1329 array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'vigilante-section-audit-alerts', 'label' => __( 'Time window', 'vigilante' ), 'label_en' => 'Time window', 'keywords' => _x( 'time window', 'settings search keywords', 'vigilante' ) ),
1330 array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'vigilante-section-audit-alerts', 'label' => __( 'Thresholds per category', 'vigilante' ), 'label_en' => 'Thresholds per category', 'keywords' => _x( 'thresholds per category threshold limit', 'settings search keywords', 'vigilante' ) ),
1331 array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'vigilante-section-audit-alerts', 'label' => __( 'Recipients', 'vigilante' ), 'label_en' => 'Recipients', 'keywords' => _x( 'recipients email recipient', 'settings search keywords', 'vigilante' ) ),
1332 array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Automatic Scans', 'vigilante' ), 'label_en' => 'Automatic Scans', 'keywords' => _x( 'automatic scans scan scanning', 'settings search keywords', 'vigilante' ) ),
1333 array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Scan Frequency', 'vigilante' ), 'label_en' => 'Scan Frequency', 'keywords' => _x( 'scan frequency scans scanning check', 'settings search keywords', 'vigilante' ) ),
1334 array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Email Notifications', 'vigilante' ), 'label_en' => 'Email Notifications', 'keywords' => _x( 'email notifications mail notification notify', 'settings search keywords', 'vigilante' ) ),
1335 array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Test email', 'vigilante' ), 'label_en' => 'Test email', 'keywords' => _x( 'test email mail notification notify', 'settings search keywords', 'vigilante' ) ),
1336 array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Scan Scope', 'vigilante' ), 'label_en' => 'Scan Scope', 'keywords' => _x( 'scan scope scans scanning check', 'settings search keywords', 'vigilante' ) ),
1337 array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Excluded Paths', 'vigilante' ), 'label_en' => 'Excluded Paths', 'keywords' => _x( 'excluded paths exclude exclusions ignore ignored path folder folders', 'settings search keywords', 'vigilante' ) ),
1338 array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Excluded Extensions', 'vigilante' ), 'label_en' => 'Excluded Extensions', 'keywords' => _x( 'excluded extensions exclude exclusions ignore ignored extension filetype', 'settings search keywords', 'vigilante' ) ),
1339 );
1340 }
1341
1342 /**
1343 * Enqueue admin assets
1344 *
1345 * @param string $hook Current admin page.
1346 */
1347 public function enqueue_assets( $hook ) {
1348 // toplevel_page_vigilante for top-level menu page
1349 if ( 'toplevel_page_vigilante' !== $hook ) {
1350 return;
1351 }
1352
1353 wp_enqueue_style(
1354 'vigilante-admin',
1355 VIGILANTE_ASSETS_URL . 'css/admin.css',
1356 array(),
1357 VIGILANTE_VERSION
1358 );
1359
1360 wp_enqueue_script(
1361 'vigilante-admin',
1362 VIGILANTE_ASSETS_URL . 'js/admin.js',
1363 array( 'jquery' ),
1364 VIGILANTE_VERSION,
1365 true
1366 );
1367
1368 wp_localize_script( 'vigilante-admin', 'vigilanteAdmin', array(
1369 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
1370 'nonce' => wp_create_nonce( 'vigilante_admin_nonce' ),
1371 'currentUserId' => get_current_user_id(),
1372 'logoutUrl' => wp_logout_url( wp_login_url() ),
1373 'adminUrl' => admin_url( 'admin.php?page=vigilante' ),
1374 'searchIndex' => $this->get_search_index(),
1375 'underAttack' => array(
1376 'active' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->is_active(),
1377 'remaining' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->get_remaining_time(),
1378 ),
1379 'strings' => array(
1380 'saving' => __( 'Saving...', 'vigilante' ),
1381 'sendingTest' => __( 'Sending...', 'vigilante' ),
1382 'saved' => __( 'Settings saved', 'vigilante' ),
1383 'error' => __( 'Error saving settings', 'vigilante' ),
1384 'confirm' => __( 'Are you sure?', 'vigilante' ),
1385 'scanning' => __( 'Scanning...', 'vigilante' ),
1386 'scanComplete' => __( 'Scan complete', 'vigilante' ),
1387 'loading' => __( 'Loading...', 'vigilante' ),
1388 'searching' => __( 'Searching...', 'vigilante' ),
1389 'noUsersFound' => __( 'No users found', 'vigilante' ),
1390 'searchError' => __( 'Error searching users', 'vigilante' ),
1391 'sending' => __( 'Sending...', 'vigilante' ),
1392 'sendNotification' => __( 'Send notification now', 'vigilante' ),
1393 'notificationsSent' => __( 'notifications sent', 'vigilante' ),
1394 'skipped' => __( 'skipped', 'vigilante' ),
1395 'failed' => __( 'failed', 'vigilante' ),
1396 'customConfig' => __( 'Custom Configuration', 'vigilante' ),
1397 // Header tester strings
1398 'testHeaders' => __( 'Test Headers', 'vigilante' ),
1399 'testing' => __( 'Testing...', 'vigilante' ),
1400 'score' => __( 'Score', 'vigilante' ),
1401 'enabledHeaders' => __( 'Enabled headers', 'vigilante' ),
1402 'missingHeaders' => __( 'Missing headers', 'vigilante' ),
1403 'warnings' => __( 'Warnings', 'vigilante' ),
1404 // File integrity scan results strings
1405 'scanResults' => __( 'Scan Results', 'vigilante' ),
1406 'ok' => __( 'OK', 'vigilante' ),
1407 'modified' => __( 'Modified', 'vigilante' ),
1408 'suspicious' => __( 'Suspicious', 'vigilante' ),
1409 'totalScanned' => __( 'Total Scanned', 'vigilante' ),
1410 'suspiciousFiles' => __( 'Suspicious Files', 'vigilante' ),
1411 'suspiciousWarning' => __( 'These files may contain malicious code or are in unexpected locations. Review immediately!', 'vigilante' ),
1412 'file' => __( 'File', 'vigilante' ),
1413 'reason' => __( 'Reason', 'vigilante' ),
1414 'type' => __( 'Type', 'vigilante' ),
1415 'unknown' => __( 'Unknown', 'vigilante' ),
1416 'modifiedFiles' => __( 'Modified Files', 'vigilante' ),
1417 'modifiedDescription' => __( 'These files (apparently) differ from the original WordPress or plugin versions.', 'vigilante' ),
1418 'extraFiles' => __( 'Extra Files', 'vigilante' ),
1419 'extra' => __( 'Extra', 'vigilante' ),
1420 'ignored' => __( 'Ignored', 'vigilante' ),
1421 'extraDescription' => __( 'PHP files found in plugins or themes that are not part of the original distribution from WordPress.org.', 'vigilante' ),
1422 'actions' => __( 'Actions', 'vigilante' ),
1423 'ignore' => __( 'Ignore', 'vigilante' ),
1424 'ignoring' => __( 'Ignoring...', 'vigilante' ),
1425 'fileIgnored' => __( 'File added to ignored list.', 'vigilante' ),
1426 'fileUnignored' => __( 'File removed from ignored list.', 'vigilante' ),
1427 'confirmClearIgnored' => __( 'Remove all files from the ignored list? They will appear in scan results again.', 'vigilante' ),
1428 'ignoredCleared' => __( 'Ignored files list cleared. Page will reload...', 'vigilante' ),
1429 'selectAll' => __( 'Select all', 'vigilante' ),
1430 'bulkIgnoreSelected' => __( 'Ignore selected', 'vigilante' ),
1431 'bulkUnignoreSelected'=> __( 'Stop ignoring selected', 'vigilante' ),
1432 'bulkNoSelection' => __( 'Select at least one file first.', 'vigilante' ),
1433 'bulkConfirmIgnore' => __( 'Ignore the selected files? They will be hidden from future scan results until you remove them from the ignored list.', 'vigilante' ),
1434 'bulkConfirmUnignore' => __( 'Remove the selected files from the ignored list? They will appear in scan results again.', 'vigilante' ),
1435 'bulkProcessing' => __( 'Processing...', 'vigilante' ),
1436 /* translators: %d: number of files selected for bulk action. */
1437 'bulkSelectedCount' => __( '%d selected', 'vigilante' ),
1438 'allClear' => __( 'All files verified - no issues found!', 'vigilante' ),
1439 'criticalConfigTitle' => __( 'Critical config files modified', 'vigilante' ),
1440 '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' ),
1441 'approve' => __( 'Approve', 'vigilante' ),
1442 'approving' => __( 'Approving...', 'vigilante' ),
1443 'criticalApproved' => __( 'Change approved. Next scan will use the current state as baseline.', 'vigilante' ),
1444 'reviewChanges' => __( 'Review changes', 'vigilante' ),
1445 'hideChanges' => __( 'Hide changes', 'vigilante' ),
1446 'changes' => __( 'Changes', 'vigilante' ),
1447 'diffUnavailable' => __( 'Diff not available for this file (baseline was created before diff tracking was added). Approve to enable diff on future changes.', 'vigilante' ),
1448 'diffEmpty' => __( 'No line-level changes detected (may be whitespace or reordering).', 'vigilante' ),
1449 'diffLines' => __( 'lines', 'vigilante' ),
1450 // Under Attack mode strings
1451 'underAttackConfirmActivate' => __( 'Activate Under Attack mode? All visitors will see a verification page for the next 4 hours.', 'vigilante' ),
1452 'underAttackConfirmDeactivate' => __( 'Deactivate Under Attack mode?', 'vigilante' ),
1453 'underAttackActivating' => __( 'Activating...', 'vigilante' ),
1454 'underAttackDeactivating' => __( 'Deactivating...', 'vigilante' ),
1455 // Database backup strings
1456 'dbBackupDownloading' => __( 'Generating backup...', 'vigilante' ),
1457 'dbBackupNoTables' => __( 'Please select at least one table.', 'vigilante' ),
1458 'dbBackupSuccess' => __( 'Database backup downloaded successfully.', 'vigilante' ),
1459 // Firewall unblock
1460 'confirmUnblockIp' => __( 'Unblock this IP from firewall rate limiting?', 'vigilante' ),
1461 // Database prefix strings
1462 'dbPrefixConfirm' => __( 'This operation will change your database prefix. It is irreversible. Make sure you have a current database backup before proceeding.', 'vigilante' ),
1463 'dbPrefixChanging' => __( 'Changing prefix...', 'vigilante' ),
1464 'dbPrefixSuccess' => __( 'Database prefix changed successfully. The page will reload now.', 'vigilante' ),
1465 'dbPrefixCheckbox' => __( 'You must confirm that you have a database backup.', 'vigilante' ),
1466 /* translators: 1: Hours, 2: Minutes */
1467 'underAttackRemaining' => __( '%1$dh %2$dm remaining', 'vigilante' ),
1468 'underAttackLabel' => __( 'Under Attack', 'vigilante' ),
1469 'standardLabel' => __( 'Standard', 'vigilante' ),
1470 'maximumLabel' => __( 'Maximum Security', 'vigilante' ),
1471 'deactivate' => __( 'Deactivate', 'vigilante' ),
1472 'underAttackActivate' => __( 'Activate for 4 hours', 'vigilante' ),
1473 // Settings strings
1474 'saveSettings' => __( 'Save Settings', 'vigilante' ),
1475 'settingsResetDefaults' => __( 'Settings reset to defaults.', 'vigilante' ),
1476 'confirmOverwrite' => __( 'This will overwrite your current settings.', 'vigilante' ),
1477 'importFailed' => __( 'Could not import settings. Check the file and try again.', 'vigilante' ),
1478 /* translators: 1: tests passed, 2: total tests in this category */
1479 'testsCounter' => __( '%1$d/%2$d tests', 'vigilante' ),
1480 'confirmResetAll' => __( 'This will reset ALL settings to defaults.', 'vigilante' ),
1481 'couldNotDetermineSection' => __( 'Could not determine section.', 'vigilante' ),
1482 'confirmResetSection' => __( 'Reset this section to default values? This cannot be undone.', 'vigilante' ),
1483 'sectionResetDefaults' => __( 'Section reset to defaults.', 'vigilante' ),
1484 /* translators: %s: preset name */
1485 'confirmApplyPreset' => __( 'Apply the "%s" preset?', 'vigilante' ),
1486 // Scan strings
1487 'scanFailed' => __( 'Scan failed', 'vigilante' ),
1488 /* translators: %s: error message */
1489 'scanError' => __( 'Scan error: %s', 'vigilante' ),
1490 'runScanNow' => __( 'Run Scan Now', 'vigilante' ),
1491 'confirmClearScan' => __( 'Are you sure you want to clear all scan results?', 'vigilante' ),
1492 'clearing' => __( 'Clearing...', 'vigilante' ),
1493 'scanResultsCleared' => __( 'Scan results cleared. Page will reload...', 'vigilante' ),
1494 'failedClearResults' => __( 'Failed to clear results', 'vigilante' ),
1495 /* translators: %s: error message */
1496 'ajaxError' => __( 'AJAX Error: %s', 'vigilante' ),
1497 // Activity log popup strings
1498 'logRequest' => __( 'Request', 'vigilante' ),
1499 'logDate' => __( 'Date', 'vigilante' ),
1500 'logMethod' => __( 'Method', 'vigilante' ),
1501 'logType' => __( 'Type', 'vigilante' ),
1502 'logAction' => __( 'Action', 'vigilante' ),
1503 'logSeverity' => __( 'Severity', 'vigilante' ),
1504 'logMessage' => __( 'Message', 'vigilante' ),
1505 'logClient' => __( 'Client', 'vigilante' ),
1506 'logUser' => __( 'User', 'vigilante' ),
1507 'logIpAddress' => __( 'IP Address', 'vigilante' ),
1508 'logUserAgent' => __( 'User Agent', 'vigilante' ),
1509 'logIpLabel' => __( 'IP:', 'vigilante' ),
1510 'logUaLabel' => __( 'UA:', 'vigilante' ),
1511 'logWhitelist' => __( 'Whitelist', 'vigilante' ),
1512 'logBlacklist' => __( 'Blacklist', 'vigilante' ),
1513 'logInWhitelist' => __( 'In whitelist', 'vigilante' ),
1514 'logInBlacklist' => __( 'In blacklist', 'vigilante' ),
1515 'logAdded' => __( 'Added!', 'vigilante' ),
1516 'logErrorAddingToList' => __( 'Error adding to list', 'vigilante' ),
1517 'logRequestFailed' => __( 'Request failed', 'vigilante' ),
1518 // Activity log table strings
1519 'noLogEntries' => __( 'No log entries found.', 'vigilante' ),
1520 'view' => __( 'View', 'vigilante' ),
1521 'confirmClearLogs' => __( 'This will delete all audit logs.', 'vigilante' ),
1522 // Export logs strings
1523 'exporting' => __( 'Exporting...', 'vigilante' ),
1524 /* translators: %d: number of entries */
1525 'logsExported' => __( 'Logs exported (%d entries)', 'vigilante' ),
1526 'noLogsToExport' => __( 'No logs to export', 'vigilante' ),
1527 'exportFailed' => __( 'Export failed', 'vigilante' ),
1528 'exportLogs' => __( 'Export Logs', 'vigilante' ),
1529 // Backup strings
1530 'backupCreated' => __( 'Backup created successfully.', 'vigilante' ),
1531 'createBackupNow' => __( 'Download Backup', 'vigilante' ),
1532 'downloadBackup' => __( 'Download Backup (.zip)', 'vigilante' ),
1533 /* translators: %d: number of tables */
1534 'tablesCount' => __( '%d tables', 'vigilante' ),
1535 /* translators: 1: table count, 2: human-readable size */
1536 'dbTablesTotal' => __( '%1$d tables total (%2$s)', 'vigilante' ),
1537 /* translators: 1: selected count, 2: human-readable size */
1538 'dbTablesSelected' => __( '%1$d tables selected (%2$s)', 'vigilante' ),
1539 // Settings search strings
1540 'searchNoResults' => __( 'No matching settings found.', 'vigilante' ),
1541 'searchInTab' => __( 'in', 'vigilante' ),
1542 // Modules string
1543 /* translators: 1: enabled count, 2: total count */
1544 'modulesEnabled' => __( '%1$d / %2$d modules enabled', 'vigilante' ),
1545 // Activity log label maps for JS rendering
1546 'eventTypeLabels' => array(
1547 'login' => __( 'Login', 'vigilante' ),
1548 'user' => __( 'User', 'vigilante' ),
1549 'content' => __( 'Content', 'vigilante' ),
1550 'plugin' => __( 'Plugin', 'vigilante' ),
1551 'theme' => __( 'Theme', 'vigilante' ),
1552 'settings' => __( 'Settings', 'vigilante' ),
1553 'comment' => __( 'Comment', 'vigilante' ),
1554 'media' => __( 'Media', 'vigilante' ),
1555 'firewall' => __( 'Firewall', 'vigilante' ),
1556 'file' => __( 'File', 'vigilante' ),
1557 'security' => __( 'Security', 'vigilante' ),
1558 'system' => __( 'System', 'vigilante' ),
1559 ),
1560 'severityLabels' => array(
1561 'info' => __( 'Info', 'vigilante' ),
1562 'warning' => __( 'Warning', 'vigilante' ),
1563 'critical' => __( 'Critical', 'vigilante' ),
1564 ),
1565 // Password reset strings
1566 'noUsersFoundSearch' => __( 'No users found', 'vigilante' ),
1567 /* translators: %d: number of users */
1568 'confirmForceReset' => __( 'Force password reset for %d user(s)? A password reset email will be sent to each user.', 'vigilante' ),
1569 'warningResettingSelf' => __( 'WARNING: You are including yourself. Your session will end and you will need to set a new password.', 'vigilante' ),
1570 'processing' => __( 'Processing...', 'vigilante' ),
1571 'anErrorOccurred' => __( 'An error occurred', 'vigilante' ),
1572 'forceResetSelected' => __( 'Force Reset for Selected Users', 'vigilante' ),
1573 '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' ),
1574 'warningResettingSelfAll' => __( 'WARNING: You are including yourself. Your session will end immediately.', 'vigilante' ),
1575 'forceResetAll' => __( 'Force Reset for ALL Users', 'vigilante' ),
1576 // Role-based password reset strings
1577 /* translators: %d: number of users */
1578 'confirmForceResetByRole' => __( 'Force password reset for %d user(s) with the selected roles? A password reset email will be sent to each user.', 'vigilante' ),
1579 'noRolesSelected' => __( 'Please select at least one role.', 'vigilante' ),
1580 'forceResetByRole' => __( 'Force Reset for Selected Roles', 'vigilante' ),
1581 // User approval strings
1582 'confirmApprove' => __( 'Approve this user?', 'vigilante' ),
1583 'approve' => __( 'Approve', 'vigilante' ),
1584 'rejectReason' => __( 'Enter rejection reason (optional):', 'vigilante' ),
1585 'reject' => __( 'Reject', 'vigilante' ),
1586 'noPending' => __( 'No pending registrations.', 'vigilante' ),
1587 // Session management strings
1588 'confirmRevoke' => __( 'Revoke this session?', 'vigilante' ),
1589 'confirmRevokeAll' => __( 'Revoke all other sessions?', 'vigilante' ),
1590 'revokeOthers' => __( 'Revoke All Other Sessions', 'vigilante' ),
1591 'confirmRevokeAllUser' => __( 'Revoke ALL sessions for this user? They will be logged out everywhere.', 'vigilante' ),
1592 'sessionsFor' => __( 'Sessions for:', 'vigilante' ),
1593 'noSessions' => __( 'No active sessions', 'vigilante' ),
1594 'revoke' => __( 'Revoke', 'vigilante' ),
1595 'noUsers' => __( 'No users found', 'vigilante' ),
1596 // Time ago strings
1597 'timeYear' => __( 'year', 'vigilante' ),
1598 'timeYears' => __( 'years', 'vigilante' ),
1599 'timeMonth' => __( 'month', 'vigilante' ),
1600 'timeMonths' => __( 'months', 'vigilante' ),
1601 'timeDay' => __( 'day', 'vigilante' ),
1602 'timeDays' => __( 'days', 'vigilante' ),
1603 'timeHour' => __( 'hour', 'vigilante' ),
1604 'timeHours' => __( 'hours', 'vigilante' ),
1605 'timeMinute' => __( 'minute', 'vigilante' ),
1606 'timeMinutes' => __( 'minutes', 'vigilante' ),
1607 /* translators: %1$d: count, %2$s: time unit */
1608 'timeAgo' => __( '%1$d %2$s ago', 'vigilante' ),
1609 'justNow' => __( 'Just now', 'vigilante' ),
1610 // Pagination strings
1611 /* translators: 1: first item number, 2: last item number, 3: total items */
1612 'paginationOf' => __( '%1$d–%2$d of %3$d', 'vigilante' ),
1613 'paginationEmpty' => __( '0 items', 'vigilante' ),
1614 // Security Analyzer strings
1615 'analyzerScanNow' => __( 'Scan now', 'vigilante' ),
1616 'analyzerScanning' => __( 'Scanning…', 'vigilante' ),
1617 'analyzerFastPhase' => __( 'Running fast checks…', 'vigilante' ),
1618 'analyzerSlowPhase' => __( 'Running remote checks…', 'vigilante' ),
1619 'analyzerScanComplete' => __( 'Security scan complete.', 'vigilante' ),
1620 'analyzerScanFailed' => __( 'Security scan failed.', 'vigilante' ),
1621 'analyzerShowDetails' => __( 'Show detailed breakdown', 'vigilante' ),
1622 'analyzerHideDetails' => __( 'Hide detailed breakdown', 'vigilante' ),
1623 'analyzerGoToSetting' => __( 'Go to setting', 'vigilante' ),
1624 'analyzerNoData' => __( 'No data yet — run a scan to populate this category.', 'vigilante' ),
1625 'analyzerJustNow' => __( 'just now', 'vigilante' ),
1626 'analyzerAgo' => __( 'ago', 'vigilante' ),
1627 'analyzerSettingsSaved' => __( 'Analyzer settings saved.', 'vigilante' ),
1628 'analyzerLastScanJustNow' => __( 'Last scan just now', 'vigilante' ),
1629 'analyzerQualityExcellent' => __( 'Excellent', 'vigilante' ),
1630 'analyzerQualityGood' => __( 'Good', 'vigilante' ),
1631 'analyzerQualityFair' => __( 'Fair', 'vigilante' ),
1632 'analyzerQualityPoor' => __( 'Poor', 'vigilante' ),
1633 'analyzerQualityCritical' => __( 'Critical', 'vigilante' ),
1634 'analyzerPts' => __( 'pts', 'vigilante' ),
1635 'analyzerLearnMore' => __( 'Learn more', 'vigilante' ),
1636 'analyzerInfoAllClear' => __( 'All clear', 'vigilante' ),
1637 /* translators: %d: number of findings in an info-only category */
1638 'analyzerInfoFindings' => __( '%d findings', 'vigilante' ),
1639 ),
1640 ) );
1641
1642 // 2FA Admin assets
1643 wp_enqueue_style(
1644 'vigilante-2fa-admin',
1645 VIGILANTE_ASSETS_URL . 'css/two-factor-admin.css',
1646 array( 'vigilante-admin' ),
1647 VIGILANTE_VERSION
1648 );
1649
1650 wp_enqueue_script(
1651 'vigilante-2fa-admin',
1652 VIGILANTE_ASSETS_URL . 'js/two-factor-admin.js',
1653 array( 'jquery', 'vigilante-admin' ),
1654 VIGILANTE_VERSION,
1655 true
1656 );
1657 }
1658
1659 /**
1660 * Show admin notices
1661 */
1662 public function show_admin_notices() {
1663 // Activation notice
1664 if ( get_transient( 'vigilante_activated' ) ) {
1665 ?>
1666 <div class="notice notice-success is-dismissible vigilante-activation-notice">
1667 <p>
1668 <span class="dashicons dashicons-shield vigilante-notice-icon"></span>
1669 <strong><?php esc_html_e( 'Vigilant activated successfully!', 'vigilante' ); ?></strong>
1670 <?php esc_html_e( 'Security protection is now active.', 'vigilante' ); ?>
1671 <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante' ) ); ?>">
1672 <?php esc_html_e( 'Configure settings', 'vigilante' ); ?>
1673 </a>
1674 </p>
1675 </div>
1676 <?php
1677 delete_transient( 'vigilante_activated' );
1678 }
1679
1680 // Under Attack mode notice (non-dismissible, shown on all admin pages)
1681 $ua_status = get_option( Vigilante_Under_Attack::OPTION_NAME, array() );
1682 if ( ! empty( $ua_status['active'] ) ) {
1683 $ua_remaining = ( $ua_status['activated_at'] + $ua_status['duration'] ) - time();
1684 if ( $ua_remaining > 0 ) {
1685 $ua_hours = floor( $ua_remaining / 3600 );
1686 $ua_mins = floor( ( $ua_remaining % 3600 ) / 60 );
1687 $dashboard_url = admin_url( 'admin.php?page=vigilante' );
1688 ?>
1689 <div class="notice notice-warning vigilante-ua-notice">
1690 <p>
1691 <span class="dashicons dashicons-shield"></span>
1692 <strong><?php esc_html_e( 'Under Attack mode is active', 'vigilante' ); ?></strong>
1693 &mdash;
1694 <?php
1695 printf(
1696 /* translators: 1: Hours, 2: Minutes */
1697 esc_html__( '%1$dh %2$dm remaining.', 'vigilante' ),
1698 absint( $ua_hours ),
1699 absint( $ua_mins )
1700 );
1701 ?>
1702 <a href="<?php echo esc_url( $dashboard_url ); ?>">
1703 <?php esc_html_e( 'Go to Vigilant dashboard', 'vigilante' ); ?>
1704 </a>
1705 </p>
1706 <p>
1707 <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>
1708 </p>
1709 </div>
1710 <?php
1711 }
1712 }
1713
1714 // Proxy/CDN detection: if IP detection is set to "direct" but requests
1715 // arrive with a forwarded-for header carrying a different valid IP, the
1716 // site is very likely behind a proxy/CDN that has not been declared, so
1717 // the firewall is seeing the proxy IP for every visitor. Guide the admin.
1718 //
1719 // Two deliberate silencers (2.9.2):
1720 // - A loopback forwarded IP (::1 / 127.x) means the person browsing IS
1721 // the machine itself: that only happens in local development stacks
1722 // (Local's nginx router, Docker...), never behind a production
1723 // proxy/CDN, so the notice would be pure noise there.
1724 // - The notice is dismissible and the dismissal persists site-wide via
1725 // the vigilante_dismissed_notices option (the admin evaluated it and
1726 // decided; nagging forever helps nobody).
1727 $dismissed_notices = get_option( 'vigilante_dismissed_notices', array() );
1728 if (
1729 current_user_can( 'manage_options' )
1730 && '' === Vigilante_IP_Utils::trusted_proxy_header()
1731 && ! isset( $dismissed_notices['proxy_detection'] )
1732 ) {
1733 $remote = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
1734 $detected = '';
1735 foreach ( Vigilante_IP_Utils::trusted_header_map() as $proxy_label => $server_key ) {
1736 if ( empty( $_SERVER[ $server_key ] ) ) {
1737 continue;
1738 }
1739 $candidate = sanitize_text_field( wp_unslash( $_SERVER[ $server_key ] ) );
1740 if ( false !== strpos( $candidate, ',' ) ) {
1741 $parts = explode( ',', $candidate );
1742 $candidate = trim( $parts[0] );
1743 }
1744 if ( ! filter_var( $candidate, FILTER_VALIDATE_IP ) || $candidate === $remote ) {
1745 continue;
1746 }
1747 // Loopback forwarded IP = local development, not a real proxy.
1748 if ( '::1' === $candidate || 0 === strpos( $candidate, '127.' ) ) {
1749 continue;
1750 }
1751 $detected = $proxy_label;
1752 break;
1753 }
1754
1755 if ( '' !== $detected ) {
1756 $firewall_url = admin_url( 'admin.php?page=vigilante&tab=firewall' );
1757 $detail_message = sprintf(
1758 /* translators: %s: detected forwarded header name wrapped in a code tag. */
1759 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' ),
1760 '<code>' . esc_html( strtoupper( $detected ) ) . '</code>'
1761 );
1762 ?>
1763 <div class="notice notice-warning is-dismissible vigilante-proxy-notice">
1764 <p>
1765 <span class="dashicons dashicons-shield"></span>
1766 <strong><?php esc_html_e( 'Vigilant: this site looks like it is behind a proxy or CDN', 'vigilante' ); ?></strong>
1767 </p>
1768 <p><?php echo wp_kses_post( $detail_message ); ?></p>
1769 <p>
1770 <a href="<?php echo esc_url( $firewall_url ); ?>" class="button button-secondary">
1771 <?php esc_html_e( 'Set your proxy in Firewall settings', 'vigilante' ); ?>
1772 </a>
1773 </p>
1774 </div>
1775 <script>
1776 jQuery( function ( $ ) {
1777 $( document ).on( 'click', '.vigilante-proxy-notice .notice-dismiss', function () {
1778 $.post( ajaxurl, {
1779 action: 'vigilante_dismiss_notice',
1780 notice_id: 'proxy_detection',
1781 nonce: <?php echo wp_json_encode( wp_create_nonce( 'vigilante_dismiss_notice' ) ); ?>
1782 } );
1783 } );
1784 } );
1785 </script>
1786 <?php
1787 }
1788 }
1789 }
1790
1791 /**
1792 * Render settings page
1793 */
1794 public function render_settings_page() {
1795 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1796 $this->current_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'dashboard';
1797
1798 if ( ! array_key_exists( $this->current_tab, $this->tabs ) ) {
1799 $this->current_tab = 'dashboard';
1800 }
1801 ?>
1802 <div class="wrap vigilante-admin-wrap">
1803 <div class="vigilante-header-row">
1804 <h1 class="vigilante-page-title">
1805 <img src="<?php echo esc_url( VIGILANTE_ASSETS_URL . 'images/icon.png' ); ?>" alt="Vigilante" class="vigilante-title-icon">
1806 <?php esc_html_e( 'Vigilant', 'vigilante' ); ?>
1807 <span class="vigilante-version">v<?php echo esc_html( VIGILANTE_VERSION ); ?></span>
1808 </h1>
1809 <div class="vigilante-search-wrapper">
1810 <div class="vigilante-search-input-wrap">
1811 <span class="vigilante-search-icon dashicons dashicons-search" aria-hidden="true"></span>
1812 <input type="search" id="vigilante-settings-search" class="vigilante-settings-search" placeholder="<?php esc_attr_e( 'Search settings…', 'vigilante' ); ?>" autocomplete="off">
1813 <span class="vigilante-search-shortcut" aria-hidden="true">/</span>
1814 </div>
1815 <div id="vigilante-settings-search-results" class="vigilante-search-results" hidden role="listbox"></div>
1816 </div>
1817 </div>
1818
1819 <?php $this->render_tabs(); ?>
1820
1821 <div class="vigilante-content">
1822 <div class="vigilante-main">
1823 <?php $this->render_tab_content(); ?>
1824 </div>
1825 <div class="vigilante-sidebar">
1826 <?php $this->render_sidebar(); ?>
1827 </div>
1828 </div>
1829 </div>
1830 <?php
1831 }
1832
1833 /**
1834 * Render tabs navigation
1835 */
1836 private function render_tabs() {
1837 $options = $this->settings->get_all_options();
1838
1839 // Map tabs to modules
1840 $tab_to_module = array(
1841 'firewall' => 'firewall',
1842 'headers' => 'security_headers',
1843 'login' => 'login_security',
1844 'rest-api' => 'rest_api_security',
1845 'users' => 'user_security',
1846 'wp-hardening' => 'wp_hardening',
1847 'file-integrity' => 'file_integrity',
1848 'activity-log' => 'activity_log',
1849 );
1850 ?>
1851 <nav class="nav-tab-wrapper vigilante-nav-tabs">
1852 <?php foreach ( $this->tabs as $tab_id => $tab_name ) :
1853 $is_disabled = false;
1854 $module = isset( $tab_to_module[ $tab_id ] ) ? $tab_to_module[ $tab_id ] : null;
1855 if ( $module && empty( $options['modules'][ $module ] ) ) {
1856 $is_disabled = true;
1857 }
1858 ?>
1859 <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=' . $tab_id ) ); ?>"
1860 class="nav-tab <?php echo $this->current_tab === $tab_id ? 'nav-tab-active' : ''; ?> <?php echo $is_disabled ? 'vigilante-tab-disabled' : ''; ?>"
1861 <?php if ( $is_disabled ) : ?>title="<?php esc_attr_e( 'Module Disabled', 'vigilante' ); ?>"<?php endif; ?>>
1862 <?php echo esc_html( $tab_name ); ?>
1863 <?php if ( $is_disabled ) : ?><span class="vigilante-tab-off">OFF</span><?php endif; ?>
1864 </a>
1865 <?php endforeach; ?>
1866 </nav>
1867 <?php
1868 }
1869
1870 /**
1871 * Render current tab content
1872 */
1873 private function render_tab_content() {
1874 $method = 'render_tab_' . str_replace( '-', '_', $this->current_tab );
1875
1876 if ( method_exists( $this, $method ) ) {
1877 $this->$method();
1878 } else {
1879 $this->render_tab_coming_soon();
1880 }
1881 }
1882
1883 /**
1884 * Render coming soon placeholder
1885 */
1886 private function render_tab_coming_soon() {
1887 ?>
1888 <div class="vigilante-settings-section">
1889 <h2><?php esc_html_e( 'Coming Soon', 'vigilante' ); ?></h2>
1890 <p><?php esc_html_e( 'This section is under development.', 'vigilante' ); ?></p>
1891 </div>
1892 <?php
1893 }
1894
1895 /**
1896 * Values to display for a section that this site does not control
1897 *
1898 * On a subsite the stored options are its own copy, which nothing acts on:
1899 * wp-config.php and .htaccess are written from the main site. Painting the
1900 * local copy describes a configuration that is not running, so a subsite
1901 * admin sees a box ticked here and the constant absent from the file, or the
1902 * other way round. Read the main site's values instead, which are the ones in
1903 * force, and fall back to the local ones if they cannot be read.
1904 *
1905 * @since 2.9.8
1906 *
1907 * @param string $section Settings section.
1908 * @return array
1909 */
1910 private function get_section_for_display( $section ) {
1911 $local = $this->settings->get_section( $section );
1912
1913 if ( ! $this->shared_files_locked() ) {
1914 return $local;
1915 }
1916
1917 // shared_files_locked() is only true on multisite, where get_blog_option() exists.
1918 $main = get_blog_option( get_main_site_id(), Vigilante_Settings::OPTION_NAME, array() );
1919
1920 if ( ! is_array( $main ) || empty( $main[ $section ] ) || ! is_array( $main[ $section ] ) ) {
1921 return $local;
1922 }
1923
1924 return wp_parse_args( $main[ $section ], $local );
1925 }
1926
1927 /**
1928 * Whether the sections that write wp-config.php and .htaccess are read-only here
1929 *
1930 * True on a network when this is not the main site, or the user is not a
1931 * network administrator. See Vigilante_Settings::can_write_shared_files().
1932 *
1933 * @since 2.9.8
1934 *
1935 * @return bool
1936 */
1937 private function shared_files_locked() {
1938 return ! Vigilante_Settings::can_write_shared_files();
1939 }
1940
1941 /**
1942 * Print the shared-files notice for a section that cannot be edited here
1943 *
1944 * @since 2.9.8
1945 */
1946 private function render_shared_files_notice() {
1947 if ( ! $this->shared_files_locked() ) {
1948 return;
1949 }
1950 ?>
1951 <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;">
1952 <p style="margin:0;"><?php echo esc_html( Vigilante_Settings::get_shared_files_notice() ); ?></p>
1953 </div>
1954 <?php
1955 }
1956
1957 /**
1958 * Check if module is disabled and render warning
1959 *
1960 * @param string $module_key Module key.
1961 * @return bool True if disabled.
1962 */
1963 private function render_module_disabled_notice( $module_key ) {
1964 if ( $this->settings->is_module_enabled( $module_key ) ) {
1965 return false;
1966 }
1967
1968 $module_labels = $this->settings->get_module_labels();
1969 $module_name = isset( $module_labels[ $module_key ] ) ? $module_labels[ $module_key ] : $module_key;
1970 ?>
1971 <div class="notice notice-warning vigilante-module-disabled-notice">
1972 <p>
1973 <strong><?php esc_html_e( 'Module Disabled', 'vigilante' ); ?></strong> -
1974 <?php
1975 printf(
1976 /* translators: %s: Module name */
1977 esc_html__( 'The %s module is currently disabled. Enable it from the Dashboard to use these settings.', 'vigilante' ),
1978 esc_html( $module_name )
1979 );
1980 ?>
1981 </p>
1982 <p>
1983 <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=dashboard' ) ); ?>" class="button">
1984 <?php esc_html_e( 'Go to Dashboard', 'vigilante' ); ?>
1985 </a>
1986 </p>
1987 </div>
1988 <?php
1989 return true;
1990 }
1991
1992 /**
1993 * Render the Security Analyzer (Security Check) widget + expandable full report.
1994 *
1995 * Lives in the Dashboard tab between the Configuration Score status card and
1996 * the modules grid. Uses the last persisted scan to hydrate server-side so the
1997 * first paint shows real data; "Scan now" runs the 2-phase AJAX to refresh.
1998 *
1999 * @param array $last_scan Result of Vigilante_Security_Analyzer::get_last_scan().
2000 * @param array $history Score history (oldest first).
2001 * @param array $categories_def Category metadata (slug => label/max).
2002 * @param array $analyzer_settings Settings subsection for weekly cron + email.
2003 */
2004 private function render_analyzer_widget( $last_scan, $history, $categories_def, $analyzer_settings ) {
2005 $has_data = ! empty( $last_scan['ran_at'] );
2006 $score = isset( $last_scan['score'] ) ? (int) $last_scan['score'] : 0;
2007 $grade = isset( $last_scan['grade'] ) ? (string) $last_scan['grade'] : '';
2008 $counts = isset( $last_scan['counts'] ) && is_array( $last_scan['counts'] ) ? $last_scan['counts'] : array();
2009 $ran_at_human = $has_data
2010 ? sprintf(
2011 /* translators: %s: relative time like "2 hours" */
2012 __( 'Last scan %s ago', 'vigilante' ),
2013 human_time_diff( (int) $last_scan['ran_at'], time() )
2014 )
2015 : __( 'Never scanned', 'vigilante' );
2016 $categories = isset( $last_scan['categories'] ) && is_array( $last_scan['categories'] ) ? $last_scan['categories'] : array();
2017 $weekly_enabled = ! isset( $analyzer_settings['weekly_scan_enabled'] ) || ! empty( $analyzer_settings['weekly_scan_enabled'] );
2018 $email_enabled = ! empty( $analyzer_settings['email_on_regression'] );
2019
2020 $quality = self::analyzer_quality_tag( $score );
2021 ?>
2022 <div class="vigilante-analyzer" id="vigilante-analyzer"
2023 data-has-data="<?php echo $has_data ? '1' : '0'; ?>">
2024 <div class="vigilante-analyzer-header">
2025 <div class="vigilante-analyzer-title">
2026 <h2>
2027 <span class="dashicons dashicons-shield-alt" aria-hidden="true"></span>
2028 <?php esc_html_e( 'Security Check', 'vigilante' ); ?>
2029 </h2>
2030 <p class="description">
2031 <?php esc_html_e( 'On-demand audit of what an attacker would see right now, plus 13 internal checks impossible from the outside.', 'vigilante' ); ?>
2032 </p>
2033 <p class="vigilante-analyzer-last-scan" data-role="ran-at">
2034 <span class="dashicons dashicons-clock" aria-hidden="true"></span>
2035 <?php echo esc_html( $ran_at_human ); ?>
2036 </p>
2037 </div>
2038 <div class="vigilante-analyzer-actions">
2039 <button type="button" class="button button-primary" id="vigilante-analyzer-scan">
2040 <span class="dashicons dashicons-update" aria-hidden="true"></span>
2041 <?php esc_html_e( 'Scan now', 'vigilante' ); ?>
2042 </button>
2043 </div>
2044 </div>
2045
2046 <div class="vigilante-analyzer-summary">
2047 <div class="vigilante-analyzer-score-card">
2048 <?php if ( $has_data && $grade ) : ?>
2049 <div class="vigilante-score-circle vigilante-grade-<?php echo esc_attr( strtolower( $grade ) ); ?>">
2050 <span class="vigilante-grade"><?php echo esc_html( $grade ); ?></span>
2051 <span class="vigilante-score-text"><?php echo esc_html( $score ); ?>%</span>
2052 </div>
2053 <?php else : ?>
2054 <div class="vigilante-score-circle vigilante-grade-empty">
2055 <span class="vigilante-grade">—</span>
2056 <span class="vigilante-score-text"><?php esc_html_e( 'N/A', 'vigilante' ); ?></span>
2057 </div>
2058 <?php endif; ?>
2059 <div class="vigilante-analyzer-score-meta">
2060 <p class="vigilante-analyzer-score-label">
2061 <?php esc_html_e( 'Security Score', 'vigilante' ); ?>
2062 </p>
2063 <span class="vigilante-analyzer-quality-tag vigilante-analyzer-quality-<?php echo esc_attr( $quality['slug'] ); ?>"
2064 data-role="quality-tag">
2065 <?php echo esc_html( $quality['label'] ); ?>
2066 </span>
2067 </div>
2068 </div>
2069
2070 <div class="vigilante-analyzer-counts">
2071 <span class="vigilante-analyzer-count vigilante-analyzer-count--pass">
2072 <span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>
2073 <strong data-role="pass"><?php echo esc_html( isset( $counts['pass'] ) ? $counts['pass'] : 0 ); ?></strong>
2074 <span class="vigilante-analyzer-count-label"><?php esc_html_e( 'Passed', 'vigilante' ); ?></span>
2075 </span>
2076 <span class="vigilante-analyzer-count vigilante-analyzer-count--warn">
2077 <span class="dashicons dashicons-warning" aria-hidden="true"></span>
2078 <strong data-role="warn"><?php echo esc_html( isset( $counts['warn'] ) ? $counts['warn'] : 0 ); ?></strong>
2079 <span class="vigilante-analyzer-count-label"><?php esc_html_e( 'Warnings', 'vigilante' ); ?></span>
2080 </span>
2081 <span class="vigilante-analyzer-count vigilante-analyzer-count--fail">
2082 <span class="dashicons dashicons-dismiss" aria-hidden="true"></span>
2083 <strong data-role="fail"><?php echo esc_html( isset( $counts['fail'] ) ? $counts['fail'] : 0 ); ?></strong>
2084 <span class="vigilante-analyzer-count-label"><?php esc_html_e( 'Failing', 'vigilante' ); ?></span>
2085 </span>
2086 </div>
2087
2088 <?php
2089 // Sparkline of recent scores. Require at least 3 data points so the trend
2090 // is meaningful (2 points is just a line between dots, no real trend).
2091 $hist_points = array();
2092 foreach ( $history as $h ) {
2093 $hist_points[] = (int) $h['score'];
2094 }
2095 $hist_count = count( $hist_points );
2096 if ( $hist_count >= 3 ) :
2097 $current_score = (int) end( $hist_points );
2098 $previous_score = (int) $hist_points[ $hist_count - 2 ];
2099 $delta = $current_score - $previous_score;
2100 $delta_class = $delta > 0 ? 'vigilante-analyzer-delta--up' : ( $delta < 0 ? 'vigilante-analyzer-delta--down' : 'vigilante-analyzer-delta--flat' );
2101 $delta_icon = $delta > 0 ? 'arrow-up-alt' : ( $delta < 0 ? 'arrow-down-alt' : 'minus' );
2102 if ( 0 === $delta ) {
2103 $delta_text = __( 'No change', 'vigilante' );
2104 } else {
2105 $delta_text = sprintf(
2106 /* translators: %s: signed delta, e.g. "+3" or "-5" */
2107 _n( '%s pt vs. previous scan', '%s pts vs. previous scan', abs( $delta ), 'vigilante' ),
2108 ( $delta > 0 ? '+' : '' ) . (int) $delta
2109 );
2110 }
2111 ?>
2112 <div class="vigilante-analyzer-sparkline-wrap">
2113 <div class="vigilante-analyzer-sparkline-head">
2114 <span class="vigilante-analyzer-sparkline-label">
2115 <?php
2116 echo esc_html( sprintf(
2117 /* translators: %d: number of scans */
2118 _n( 'Score trend (last %d scan)', 'Score trend (last %d scans)', $hist_count, 'vigilante' ),
2119 $hist_count
2120 ) );
2121 ?>
2122 </span>
2123 <span class="vigilante-analyzer-delta <?php echo esc_attr( $delta_class ); ?>">
2124 <span class="dashicons dashicons-<?php echo esc_attr( $delta_icon ); ?>" aria-hidden="true"></span>
2125 <?php echo esc_html( $delta_text ); ?>
2126 </span>
2127 </div>
2128 <div class="vigilante-analyzer-sparkline" data-role="sparkline"
2129 data-points="<?php echo esc_attr( wp_json_encode( $hist_points ) ); ?>">
2130 <?php echo self::sparkline_svg( $hist_points ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Safe SVG from helper ?>
2131 </div>
2132 </div>
2133 <?php elseif ( $has_data ) : ?>
2134 <div class="vigilante-analyzer-sparkline-wrap vigilante-analyzer-sparkline-wrap--placeholder">
2135 <span class="vigilante-analyzer-sparkline-label">
2136 <?php esc_html_e( 'Score trend', 'vigilante' ); ?>
2137 </span>
2138 <p class="vigilante-analyzer-sparkline-hint">
2139 <span class="dashicons dashicons-chart-line" aria-hidden="true"></span>
2140 <?php
2141 $needed = 3 - $hist_count;
2142 echo esc_html( sprintf(
2143 /* translators: %d: number of additional scans needed */
2144 _n( '%d more scan needed to show a trend.', '%d more scans needed to show a trend.', $needed, 'vigilante' ),
2145 $needed
2146 ) );
2147 ?>
2148 </p>
2149 </div>
2150 <?php endif; ?>
2151 </div>
2152
2153 <div class="vigilante-analyzer-toggle-row">
2154 <button type="button" class="button-link vigilante-analyzer-toggle" aria-expanded="false">
2155 <?php esc_html_e( 'Show detailed breakdown', 'vigilante' ); ?>
2156 <span class="vigilante-analyzer-toggle-chevron" aria-hidden="true"></span>
2157 </button>
2158 </div>
2159
2160 <div class="vigilante-analyzer-details" hidden>
2161 <div class="vigilante-analyzer-categories" data-role="categories">
2162 <?php foreach ( $categories_def as $slug => $meta ) :
2163 $cat = isset( $categories[ $slug ] ) ? $categories[ $slug ] : array();
2164 $earned = isset( $cat['earned'] ) ? (int) $cat['earned'] : 0;
2165 // Always use the declared meta as the source of truth for the maximum.
2166 // The cached scan may carry an old max if a check was added/removed
2167 // between releases (see 2.6.1: closed_plugins raised internal from 22 to 28
2168 // but cached scans still reported max=22 until reset).
2169 $cat_max = (int) $meta['max'];
2170 $info_only = ! empty( $meta['info_only'] ) || 0 === (int) $meta['max'];
2171 $cat_pct = $cat_max > 0 ? (int) round( ( $earned / $cat_max ) * 100 ) : 0;
2172 $checks = isset( $cat['checks'] ) ? (array) $cat['checks'] : array();
2173 $cat_counts = isset( $cat['counts'] ) && is_array( $cat['counts'] ) ? $cat['counts'] : array( 'pass' => 0, 'warn' => 0, 'fail' => 0, 'info' => 0 );
2174 $cat_quality = self::analyzer_quality_tag( $cat_pct );
2175 $info_count = isset( $cat_counts['info'] ) ? (int) $cat_counts['info'] : 0;
2176 ?>
2177 <details class="vigilante-analyzer-category<?php echo $info_only ? ' vigilante-analyzer-category--info' : ''; ?>"
2178 data-category="<?php echo esc_attr( $slug ); ?>"
2179 data-info-only="<?php echo $info_only ? '1' : '0'; ?>">
2180 <summary class="vigilante-analyzer-category-summary">
2181 <span class="vigilante-analyzer-category-chevron" aria-hidden="true"></span>
2182 <span class="vigilante-analyzer-category-label"><?php echo esc_html( $meta['label'] ); ?></span>
2183 <?php if ( $info_only ) : ?>
2184 <span class="vigilante-analyzer-category-quality vigilante-analyzer-quality-info"
2185 data-role="category-quality"
2186 title="<?php esc_attr_e( 'Informational — does not affect the security score.', 'vigilante' ); ?>">
2187 <span class="dashicons dashicons-info-outline" aria-hidden="true"></span>
2188 <?php esc_html_e( 'Informational', 'vigilante' ); ?>
2189 </span>
2190 <?php else :
2191 $passed_count = (int) ( $cat_counts['pass'] ?? 0 );
2192 $scored_total = $passed_count
2193 + (int) ( $cat_counts['warn'] ?? 0 )
2194 + (int) ( $cat_counts['fail'] ?? 0 );
2195 ?>
2196 <span class="vigilante-analyzer-category-quality vigilante-analyzer-quality-<?php echo esc_attr( $cat_quality['slug'] ); ?>"
2197 data-role="category-quality">
2198 <span data-role="category-quality-label"><?php echo esc_html( $cat_quality['label'] ); ?></span>
2199 <span class="vigilante-analyzer-category-quality-sep" aria-hidden="true">·</span>
2200 <span class="vigilante-analyzer-category-tests" data-role="category-tests"
2201 data-passed="<?php echo esc_attr( $passed_count ); ?>"
2202 data-total="<?php echo esc_attr( $scored_total ); ?>">
2203 <?php
2204 echo esc_html( sprintf(
2205 /* translators: 1: tests passed, 2: total tests in this category */
2206 __( '%1$d/%2$d tests', 'vigilante' ),
2207 $passed_count,
2208 $scored_total
2209 ) );
2210 ?>
2211 </span>
2212 </span>
2213 <?php endif; ?>
2214 <?php if ( $info_only ) : ?>
2215 <span class="vigilante-analyzer-category-states" data-role="category-states">
2216 <?php if ( $info_count > 0 ) : ?>
2217 <span class="vigilante-analyzer-category-state vigilante-analyzer-category-state--info" title="<?php esc_attr_e( 'Informational', 'vigilante' ); ?>">
2218 <span data-role="state-info"><?php echo esc_html( $info_count ); ?></span><span class="dashicons dashicons-info-outline" aria-hidden="true"></span>
2219 </span>
2220 <?php endif; ?>
2221 </span>
2222 <?php endif; ?>
2223 <?php if ( ! $info_only ) : ?>
2224 <span class="vigilante-analyzer-category-score">
2225 <span data-role="earned"><?php echo esc_html( $earned ); ?></span><span class="vigilante-analyzer-category-score-sep">/</span><?php echo esc_html( $cat_max ); ?>
2226 <span class="vigilante-analyzer-category-score-unit"><?php esc_html_e( 'pts', 'vigilante' ); ?></span>
2227 </span>
2228 <span class="vigilante-analyzer-category-bar" aria-hidden="true">
2229 <span class="vigilante-analyzer-category-bar-fill vigilante-analyzer-category-bar-fill--<?php echo esc_attr( $cat_quality['slug'] ); ?>"
2230 style="width: <?php echo esc_attr( $cat_pct ); ?>%"
2231 data-role="category-bar"></span>
2232 </span>
2233 <?php else :
2234 $info_warn = isset( $cat_counts['warn'] ) ? (int) $cat_counts['warn'] : 0;
2235 $info_fail = isset( $cat_counts['fail'] ) ? (int) $cat_counts['fail'] : 0;
2236 $info_issues = $info_warn + $info_fail;
2237 if ( $info_issues > 0 ) : ?>
2238 <span class="vigilante-analyzer-category-status vigilante-analyzer-category-status--attention" data-role="info-status">
2239 <span class="dashicons dashicons-warning" aria-hidden="true"></span>
2240 <?php
2241 echo esc_html( sprintf(
2242 /* translators: %d: number of findings */
2243 _n( '%d finding', '%d findings', $info_issues, 'vigilante' ),
2244 $info_issues
2245 ) );
2246 ?>
2247 </span>
2248 <?php else : ?>
2249 <span class="vigilante-analyzer-category-status vigilante-analyzer-category-status--clear" data-role="info-status">
2250 <span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>
2251 <?php esc_html_e( 'All clear', 'vigilante' ); ?>
2252 </span>
2253 <?php endif; ?>
2254 <?php endif; ?>
2255 </summary>
2256 <ul class="vigilante-analyzer-check-list" data-role="check-list">
2257 <?php if ( empty( $checks ) ) : ?>
2258 <li class="vigilante-analyzer-check-empty">
2259 <?php esc_html_e( 'No data yet — run a scan to populate this category.', 'vigilante' ); ?>
2260 </li>
2261 <?php else : ?>
2262 <?php foreach ( $checks as $c ) : ?>
2263 <?php echo self::render_analyzer_check_row( $c ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped inside helper ?>
2264 <?php endforeach; ?>
2265 <?php endif; ?>
2266 </ul>
2267 </details>
2268 <?php endforeach; ?>
2269 </div>
2270
2271 <div class="vigilante-analyzer-weekly">
2272 <h3><?php esc_html_e( 'Automatic weekly scan', 'vigilante' ); ?></h3>
2273 <p class="description">
2274 <?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' ); ?>
2275 </p>
2276 <label class="vigilante-analyzer-toggle-option">
2277 <input type="checkbox"
2278 name="security_analyzer[weekly_scan_enabled]"
2279 value="1"
2280 <?php checked( $weekly_enabled ); ?>>
2281 <?php esc_html_e( 'Run a weekly automatic scan', 'vigilante' ); ?>
2282 </label>
2283 <label class="vigilante-analyzer-toggle-option">
2284 <input type="checkbox"
2285 name="security_analyzer[email_on_regression]"
2286 value="1"
2287 <?php checked( $email_enabled ); ?>>
2288 <?php esc_html_e( 'Email me when the score drops significantly', 'vigilante' ); ?>
2289 </label>
2290 </div>
2291 </div>
2292 </div>
2293 <?php
2294 }
2295
2296 /**
2297 * Map a 0-100 percentage to a quality tag { label, slug } aligned with the
2298 * Dashboard grade palette (a/b/c/d/e).
2299 *
2300 * @param int $pct 0..100.
2301 * @return array{label:string,slug:string}
2302 */
2303 public static function analyzer_quality_tag( $pct ) {
2304 $pct = max( 0, min( 100, (int) $pct ) );
2305 // "Excellent" is reserved for a perfect score — a single missing point drops to Good.
2306 if ( 100 === $pct ) {
2307 return array( 'label' => __( 'Excellent', 'vigilante' ), 'slug' => 'a' );
2308 }
2309 if ( $pct >= 70 ) {
2310 return array( 'label' => __( 'Good', 'vigilante' ), 'slug' => 'b' );
2311 }
2312 if ( $pct >= 50 ) {
2313 return array( 'label' => __( 'Fair', 'vigilante' ), 'slug' => 'c' );
2314 }
2315 if ( $pct >= 30 ) {
2316 return array( 'label' => __( 'Poor', 'vigilante' ), 'slug' => 'd' );
2317 }
2318 return array( 'label' => __( 'Critical', 'vigilante' ), 'slug' => 'e' );
2319 }
2320
2321 /**
2322 * Render a single analyzer check row (used both server-side and via JS template).
2323 *
2324 * @param array $check Check result array (from Vigilante_SA_Check_Result::to_array()).
2325 * @return string HTML (escaped).
2326 */
2327 private static function render_analyzer_check_row( $check ) {
2328 $id = isset( $check['id'] ) ? $check['id'] : '';
2329 $state = isset( $check['state'] ) ? $check['state'] : 'skip';
2330 $label = isset( $check['label'] ) ? $check['label'] : '';
2331 $detail = isset( $check['detail'] ) ? $check['detail'] : '';
2332 $score = isset( $check['score'] ) ? (int) $check['score'] : 0;
2333 $max = isset( $check['max'] ) ? (int) $check['max'] : 0;
2334 $fix_link = isset( $check['fix_link'] ) ? $check['fix_link'] : '';
2335
2336 $icons = array(
2337 'pass' => 'yes-alt',
2338 'warn' => 'warning',
2339 'fail' => 'dismiss',
2340 'info' => 'info',
2341 'skip' => 'minus',
2342 );
2343 $icon = isset( $icons[ $state ] ) ? $icons[ $state ] : 'minus';
2344
2345 $html = '<li class="vigilante-analyzer-check vigilante-analyzer-check--' . esc_attr( $state ) . '"';
2346 $html .= ' data-check-id="' . esc_attr( $id ) . '">';
2347 $html .= '<span class="vigilante-analyzer-check-icon dashicons dashicons-' . esc_attr( $icon ) . '" aria-hidden="true"></span>';
2348 $html .= '<div class="vigilante-analyzer-check-body">';
2349 $html .= '<div class="vigilante-analyzer-check-label">';
2350 $html .= '<span>' . esc_html( $label ) . '</span>';
2351 if ( $max > 0 && 'info' !== $state && 'skip' !== $state ) {
2352 $html .= '<span class="vigilante-analyzer-check-score">'
2353 . esc_html( $score . '/' . $max )
2354 . ' <span class="vigilante-analyzer-check-score-unit">' . esc_html__( 'pts', 'vigilante' ) . '</span>'
2355 . '</span>';
2356 }
2357 $html .= '</div>';
2358 if ( $detail ) {
2359 $html .= '<p class="vigilante-analyzer-check-detail">' . esc_html( $detail ) . '</p>';
2360 }
2361 if ( $fix_link && in_array( $state, array( 'fail', 'warn' ), true ) ) {
2362 $html .= '<a href="' . esc_url( $fix_link ) . '" class="vigilante-analyzer-fix-link">'
2363 . esc_html__( 'Go to setting', 'vigilante' )
2364 . '<span class="vigilante-analyzer-fix-arrow" aria-hidden="true">&rarr;</span></a>';
2365 } elseif ( $fix_link && 'info' === $state ) {
2366 // Info rows (e.g. DNSBL lookups) get an external "Learn more" link instead.
2367 $is_external = 0 === strpos( $fix_link, 'http' );
2368 $html .= '<a href="' . esc_url( $fix_link ) . '" class="vigilante-analyzer-fix-link"'
2369 . ( $is_external ? ' target="_blank" rel="noopener noreferrer"' : '' ) . '>'
2370 . esc_html__( 'Learn more', 'vigilante' )
2371 . '<span class="vigilante-analyzer-fix-arrow" aria-hidden="true">&rarr;</span></a>';
2372 }
2373 $html .= '</div>';
2374 $html .= '</li>';
2375 return $html;
2376 }
2377
2378 /**
2379 * Build a minimal SVG sparkline for the score history.
2380 *
2381 * @param int[] $points Score values (0..100), oldest to newest.
2382 * @return string SVG markup.
2383 */
2384 private static function sparkline_svg( $points ) {
2385 $points = array_map( 'intval', (array) $points );
2386 $count = count( $points );
2387 if ( $count < 2 ) {
2388 return '';
2389 }
2390 $width = 280;
2391 $height = 60;
2392 $padding = 4;
2393
2394 $usable_w = $width - ( $padding * 2 );
2395 $usable_h = $height - ( $padding * 2 );
2396
2397 $step = $usable_w / max( 1, $count - 1 );
2398 $max = 100; // Fixed scale — scores are 0..100.
2399
2400 $coords = array();
2401 foreach ( $points as $i => $v ) {
2402 $x = $padding + ( $i * $step );
2403 $y = $padding + ( $usable_h - ( ( $v / $max ) * $usable_h ) );
2404 $coords[] = round( $x, 2 ) . ',' . round( $y, 2 );
2405 }
2406 $path = 'M ' . implode( ' L ', $coords );
2407 $last = end( $points );
2408 $last_x = $padding + ( ( $count - 1 ) * $step );
2409 $last_y = $padding + ( $usable_h - ( ( $last / $max ) * $usable_h ) );
2410
2411 $svg = '<svg viewBox="0 0 ' . $width . ' ' . $height . '" preserveAspectRatio="none" role="img" aria-label="' . esc_attr__( 'Security Score history', 'vigilante' ) . '" focusable="false">';
2412 $svg .= '<path d="' . esc_attr( $path ) . '" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
2413 $svg .= '<circle cx="' . esc_attr( round( $last_x, 2 ) ) . '" cy="' . esc_attr( round( $last_y, 2 ) ) . '" r="3" fill="currentColor"/>';
2414 $svg .= '</svg>';
2415 return $svg;
2416 }
2417
2418 /**
2419 * Render dashboard tab
2420 */
2421 private function render_tab_dashboard() {
2422 $options = $this->settings->get_all_options();
2423 $module_labels = $this->settings->get_module_labels();
2424 $module_descriptions = $this->settings->get_module_descriptions();
2425 $presets = $this->settings->get_presets();
2426 $active_preset = get_option( 'vigilante_active_preset', '' );
2427
2428 // Calculate security score with more factors
2429 $security_score = $this->calculate_security_score( $options );
2430
2431 // Security Analyzer (v2.1.0) — hydrate widget with the last persisted scan, if any.
2432 if ( ! class_exists( 'Vigilante_Security_Analyzer' ) ) {
2433 require_once VIGILANTE_INCLUDES_DIR . 'class-security-analyzer.php';
2434 }
2435 $analyzer_instance = new Vigilante_Security_Analyzer( $this->settings, $this->activity_log );
2436 $analyzer_last_scan = $analyzer_instance->get_last_scan();
2437 $analyzer_history = $analyzer_instance->get_score_history();
2438 $analyzer_categories_def = Vigilante_Security_Analyzer::get_categories();
2439 $analyzer_settings = isset( $options['security_analyzer'] ) ? $options['security_analyzer'] : array();
2440 ?>
2441 <div class="vigilante-dashboard">
2442 <div class="vigilante-status-card">
2443 <h2><?php esc_html_e( 'Configuration Score', 'vigilante' ); ?></h2>
2444 <p class="vigilante-score-kind description">
2445 <?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' ); ?>
2446 </p>
2447 <div class="vigilante-security-score">
2448 <?php
2449 // Grade thresholds: A (90+), B (70-89), C (50-69), D (30-49), E (0-29)
2450 if ( $security_score >= 90 ) {
2451 $grade = 'A';
2452 } elseif ( $security_score >= 70 ) {
2453 $grade = 'B';
2454 } elseif ( $security_score >= 50 ) {
2455 $grade = 'C';
2456 } elseif ( $security_score >= 30 ) {
2457 $grade = 'D';
2458 } else {
2459 $grade = 'E';
2460 }
2461 ?>
2462 <div class="vigilante-score-circle vigilante-grade-<?php echo esc_attr( strtolower( $grade ) ); ?>">
2463 <span class="vigilante-grade"><?php echo esc_html( $grade ); ?></span>
2464 <span class="vigilante-score-text"><?php echo esc_html( $security_score ); ?>%</span>
2465 </div>
2466 <div class="vigilante-config-status">
2467 <?php if ( $active_preset && isset( $presets[ $active_preset ] ) ) : ?>
2468 <span class="vigilante-preset-badge vigilante-preset-<?php echo esc_attr( $active_preset ); ?>">
2469 <?php echo esc_html( $presets[ $active_preset ]['name'] ); ?>
2470 </span>
2471 <?php else : ?>
2472 <span class="vigilante-preset-badge vigilante-preset-custom">
2473 <?php esc_html_e( 'Custom Configuration', 'vigilante' ); ?>
2474 </span>
2475 <?php endif; ?>
2476 </div>
2477 </div>
2478
2479 <?php
2480 $recommendations = $this->get_security_recommendations( $options );
2481 if ( ! empty( $recommendations ) ) :
2482 ?>
2483 <div class="vigilante-recommendations">
2484 <h4><?php esc_html_e( 'Recommendations', 'vigilante' ); ?></h4>
2485 <ul class="vigilante-recommendations-grid">
2486 <?php foreach ( $recommendations as $rec ) : ?>
2487 <li>
2488 <span class="dashicons dashicons-<?php echo esc_attr( $rec['icon'] ); ?> vigilante-priority-<?php echo esc_attr( $rec['priority'] ); ?>"></span>
2489 <?php echo esc_html( $rec['message'] ); ?>
2490 <?php if ( ! empty( $rec['tab'] ) ) : ?>
2491 <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>
2492 <?php endif; ?>
2493 </li>
2494 <?php endforeach; ?>
2495 </ul>
2496 </div>
2497 <?php endif; ?>
2498 </div>
2499
2500 <?php $this->render_analyzer_widget( $analyzer_last_scan, $analyzer_history, $analyzer_categories_def, $analyzer_settings ); ?>
2501
2502 <div class="vigilante-modules-grid">
2503 <h2><?php esc_html_e( 'Security Modules', 'vigilante' ); ?></h2>
2504 <p class="description"><?php esc_html_e( 'Enable or disable security modules. Each module controls a tab with detailed settings.', 'vigilante' ); ?></p>
2505 <div class="vigilante-modules-list">
2506 <?php foreach ( $options['modules'] as $module => $enabled ) :
2507 $label = isset( $module_labels[ $module ] ) ? $module_labels[ $module ] : ucwords( str_replace( '_', ' ', $module ) );
2508 $description = isset( $module_descriptions[ $module ] ) ? $module_descriptions[ $module ] : '';
2509 ?>
2510 <div class="vigilante-module-item <?php echo $enabled ? 'enabled' : 'disabled'; ?>">
2511 <div class="vigilante-module-header">
2512 <span class="vigilante-module-status"></span>
2513 <span class="vigilante-module-name"><?php echo esc_html( $label ); ?></span>
2514 <label class="vigilante-toggle">
2515 <?php
2516 /* translators: %s: Security module name, for example Firewall. */
2517 $toggle_label = sprintf( __( 'Enable %s', 'vigilante' ), $label );
2518 ?>
2519 <input type="checkbox"
2520 name="modules[<?php echo esc_attr( $module ); ?>]"
2521 value="1"
2522 <?php checked( $enabled ); ?>
2523 aria-label="<?php echo esc_attr( $toggle_label ); ?>"
2524 data-module="<?php echo esc_attr( $module ); ?>">
2525 <span class="vigilante-toggle-slider"></span>
2526 </label>
2527 </div>
2528 <?php if ( $description ) : ?>
2529 <p class="vigilante-module-desc"><?php echo esc_html( $description ); ?></p>
2530 <?php endif; ?>
2531 </div>
2532 <?php endforeach; ?>
2533 </div>
2534 </div>
2535
2536 <div class="vigilante-presets-card">
2537 <h2><?php esc_html_e( 'Quick Configuration Presets', 'vigilante' ); ?></h2>
2538 <p><?php esc_html_e( 'Apply a preset to quickly set up recommended settings for standard or maximum security level.', 'vigilante' ); ?></p>
2539 <div class="vigilante-presets-grid">
2540 <?php foreach ( $presets as $preset_id => $preset ) :
2541 $is_active = ( $active_preset === $preset_id );
2542 ?>
2543 <div class="vigilante-preset-card <?php echo $is_active ? 'vigilante-preset-active' : ''; ?>">
2544 <?php if ( $is_active ) : ?>
2545 <span class="vigilante-active-indicator"><?php esc_html_e( 'Active', 'vigilante' ); ?></span>
2546 <?php endif; ?>
2547 <h3><?php echo esc_html( $preset['name'] ); ?></h3>
2548 <p><?php echo esc_html( $preset['description'] ); ?></p>
2549 <button type="button" class="button vigilante-preset-btn <?php echo $is_active ? 'button-primary' : ''; ?>" data-preset="<?php echo esc_attr( $preset_id ); ?>">
2550 <?php esc_html_e( 'Apply Preset', 'vigilante' ); ?>
2551 </button>
2552 </div>
2553 <?php endforeach; ?>
2554
2555 <?php
2556 // Under Attack mode card
2557 $under_attack = new Vigilante_Under_Attack( $this->settings, $this->activity_log );
2558 $ua_active = $under_attack->is_active();
2559 $ua_remaining = $under_attack->get_remaining_time();
2560 $ua_remaining_hours = floor( $ua_remaining / 3600 );
2561 $ua_remaining_mins = floor( ( $ua_remaining % 3600 ) / 60 );
2562 ?>
2563 <div class="vigilante-preset-card vigilante-under-attack-card <?php echo $ua_active ? 'vigilante-under-attack-active' : ''; ?>">
2564 <h3>
2565 <span class="dashicons dashicons-shield"></span>
2566 <?php esc_html_e( 'Under Attack', 'vigilante' ); ?>
2567 </h3>
2568 <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>
2569 <?php if ( $ua_active ) : ?>
2570 <div class="vigilante-ua-countdown" data-expires="<?php echo esc_attr( $under_attack->get_status()['activated_at'] + $under_attack->get_status()['duration'] ); ?>">
2571 <span class="dashicons dashicons-clock"></span>
2572 <span class="vigilante-ua-time">
2573 <?php
2574 printf(
2575 /* translators: 1: Hours, 2: Minutes */
2576 esc_html__( '%1$dh %2$dm remaining', 'vigilante' ),
2577 absint( $ua_remaining_hours ),
2578 absint( $ua_remaining_mins )
2579 );
2580 ?>
2581 </span>
2582 </div>
2583 <button type="button" class="button vigilante-ua-btn vigilante-ua-deactivate">
2584 <?php esc_html_e( 'Deactivate', 'vigilante' ); ?>
2585 </button>
2586 <?php else : ?>
2587 <button type="button" class="button vigilante-ua-btn vigilante-ua-activate">
2588 <?php esc_html_e( 'Activate for 4 hours', 'vigilante' ); ?>
2589 </button>
2590 <?php endif; ?>
2591 </div>
2592 </div>
2593 </div>
2594 </div>
2595 <?php
2596 }
2597
2598 /**
2599 * Render tools tab
2600 */
2601 private function render_tab_tools() {
2602 // Get current settings for notification summary
2603 $email_options = $this->settings->get_section( 'email' );
2604 $login_options = $this->settings->get_section( 'login_security' );
2605 $user_options = $this->settings->get_section( 'user_security' );
2606 $fi_options = $this->settings->get_section( 'file_integrity' );
2607 $alerts_options = $this->settings->get_section( 'audit_alerts' );
2608 $monitoring = $user_options['admin_monitoring'] ?? array();
2609 $registration = $user_options['registration_approval'] ?? array();
2610 $current_admin = get_option( 'admin_email' );
2611 $send_to_admin = ! isset( $email_options['send_to_admin_email'] ) || ! empty( $email_options['send_to_admin_email'] );
2612 $additional_raw = $email_options['additional_recipients'] ?? array();
2613 $additional = is_array( $additional_raw ) ? implode( "\n", $additional_raw ) : trim( $additional_raw );
2614 ?>
2615
2616 <!-- Notification Settings -->
2617 <div id="vigilante-section-tools-notifications" class="vigilante-settings-section vigilante-notification-section">
2618 <h2><?php esc_html_e( 'Notification settings', 'vigilante' ); ?></h2>
2619 <p><?php esc_html_e( 'Configure who receives all administrative email notifications from Vigilant. Individual notifications are enabled in their respective tabs.', 'vigilante' ); ?></p>
2620
2621 <div class="vigilante-notification-layout">
2622
2623 <!-- Left column: Recipients settings -->
2624 <div class="vigilante-notification-settings">
2625 <form class="vigilante-settings-form" data-section="email">
2626
2627 <table class="form-table vigilante-compact-form">
2628 <tr>
2629 <th scope="row"><?php esc_html_e( 'WordPress Admin Email', 'vigilante' ); ?></th>
2630 <td>
2631 <label>
2632 <input type="checkbox" name="email[send_to_admin_email]" value="1" <?php checked( $send_to_admin ); ?>>
2633 <?php
2634 printf(
2635 /* translators: %s: Admin email address */
2636 esc_html__( 'Send to admin email (%s)', 'vigilante' ),
2637 '<code>' . esc_html( $current_admin ) . '</code>'
2638 );
2639 ?>
2640 </label>
2641 </td>
2642 </tr>
2643 <tr>
2644 <th scope="row"><?php esc_html_e( 'Additional Recipients', 'vigilante' ); ?></th>
2645 <td>
2646 <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>
2647 <p class="description"><?php esc_html_e( 'One email per line.', 'vigilante' ); ?></p>
2648 </td>
2649 </tr>
2650 <tr>
2651 <th scope="row"><?php esc_html_e( 'Plugin Deactivation', 'vigilante' ); ?></th>
2652 <td>
2653 <label>
2654 <input type="checkbox" name="email[send_deactivation_email]" value="1" <?php checked( ! empty( $email_options['send_deactivation_email'] ) ); ?>>
2655 <?php esc_html_e( 'Send email when Vigilant is deactivated', 'vigilante' ); ?>
2656 </label>
2657 </td>
2658 </tr>
2659 </table>
2660
2661 <p class="submit vigilante-notification-submit">
2662 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
2663 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
2664 </button>
2665 <button type="button" class="button vigilante-test-email-btn" data-original-text="<?php esc_attr_e( 'Send test email', 'vigilante' ); ?>">
2666 <?php esc_html_e( 'Send test email', 'vigilante' ); ?>
2667 </button>
2668 <span class="vigilante-test-email-result" style="margin-left:8px;vertical-align:middle;"></span>
2669 </p>
2670 <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>
2671
2672 </form>
2673 </div>
2674
2675 <!-- Right column: Active notifications summary -->
2676 <div class="vigilante-notification-summary">
2677 <h4><?php esc_html_e( 'Active notifications', 'vigilante' ); ?></h4>
2678
2679 <table class="widefat striped">
2680 <thead>
2681 <tr>
2682 <th><?php esc_html_e( 'Notification', 'vigilante' ); ?></th>
2683 <th style="width: 1%; white-space: nowrap; text-align: center;"><?php esc_html_e( 'Status', 'vigilante' ); ?></th>
2684 <th style="width: 50px; text-align: center;"></th>
2685 </tr>
2686 </thead>
2687 <tbody>
2688 <?php
2689 $notifications = array(
2690 array(
2691 'label' => __( 'Login lockout', 'vigilante' ),
2692 'active' => ! empty( $login_options['notify_on_lockout'] ),
2693 'tab' => 'login',
2694 ),
2695 array(
2696 'label' => __( 'Administrator login', 'vigilante' ),
2697 'active' => ! empty( $login_options['notify_on_admin_login'] ),
2698 'tab' => 'login',
2699 ),
2700 array(
2701 'label' => __( 'New administrator created', 'vigilante' ),
2702 'active' => ! empty( $monitoring['alert_new_admin'] ),
2703 'tab' => 'users',
2704 ),
2705 array(
2706 'label' => __( 'Administrator email changed', 'vigilante' ),
2707 'active' => ! empty( $monitoring['alert_admin_email_change'] ),
2708 'tab' => 'users',
2709 ),
2710 array(
2711 'label' => __( 'Permission elevation', 'vigilante' ),
2712 'active' => ! empty( $monitoring['alert_permission_elevation'] ),
2713 'tab' => 'users',
2714 ),
2715 array(
2716 'label' => __( 'Admin password changed', 'vigilante' ),
2717 'active' => ! empty( $monitoring['alert_admin_password_change'] ),
2718 'tab' => 'users',
2719 ),
2720 array(
2721 'label' => __( 'Registration pending approval', 'vigilante' ),
2722 'active' => ! empty( $registration['enabled'] ) && ! empty( $registration['notify_admin'] ),
2723 'tab' => 'users',
2724 ),
2725 array(
2726 'label' => __( 'File integrity scan report', 'vigilante' ),
2727 'active' => ( $fi_options['notify_level'] ?? 'disabled' ) !== 'disabled',
2728 'tab' => 'file-integrity',
2729 ),
2730 array(
2731 'label' => __( 'File integrity instant alert', 'vigilante' ),
2732 'active' => ! empty( $fi_options['instant_alert'] ),
2733 'tab' => 'file-integrity',
2734 ),
2735 array(
2736 'label' => __( 'Audit alert: immediate', 'vigilante' ),
2737 'active' => Vigilante_Audit_Alerts::immediate_is_active( $alerts_options ),
2738 'tab' => 'activity-log',
2739 'anchor' => 'vigilante-section-audit-alerts',
2740 ),
2741 array(
2742 'label' => __( 'Audit alert: threshold', 'vigilante' ),
2743 'active' => Vigilante_Audit_Alerts::threshold_is_active( $alerts_options ),
2744 'tab' => 'activity-log',
2745 'anchor' => 'vigilante-section-audit-alerts',
2746 ),
2747 array(
2748 'label' => __( 'Under Attack mode', 'vigilante' ),
2749 'active' => true,
2750 'tab' => '',
2751 'note' => __( 'Always active', 'vigilante' ),
2752 ),
2753 array(
2754 'label' => __( 'Plugin deactivation', 'vigilante' ),
2755 'active' => ! empty( $email_options['send_deactivation_email'] ),
2756 'tab' => 'tools',
2757 ),
2758 );
2759
2760 foreach ( $notifications as $notif ) :
2761 $status_class = $notif['active'] ? 'vigilante-status-active' : 'vigilante-status-inactive';
2762 $status_label = $notif['active'] ? __( 'Active', 'vigilante' ) : __( 'Inactive', 'vigilante' );
2763 if ( ! empty( $notif['note'] ) ) {
2764 $status_label = $notif['note'];
2765 }
2766 ?>
2767 <tr>
2768 <td><?php echo esc_html( $notif['label'] ); ?></td>
2769 <td style="text-align: center; white-space: nowrap;">
2770 <span class="<?php echo esc_attr( $status_class ); ?>"><?php echo esc_html( $status_label ); ?></span>
2771 </td>
2772 <td style="text-align: center;">
2773 <?php if ( ! empty( $notif['tab'] ) ) : ?>
2774 <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=' . $notif['tab'] ) . ( ! empty( $notif['anchor'] ) ? '#' . $notif['anchor'] : '' ) ); ?>" class="button button-small">
2775 <span class="dashicons dashicons-admin-generic" style="font-size: 14px; line-height: 1.8;"></span>
2776 </a>
2777 <?php else : ?>
2778 &mdash;
2779 <?php endif; ?>
2780 </td>
2781 </tr>
2782 <?php endforeach; ?>
2783 </tbody>
2784 </table>
2785 </div>
2786
2787 </div>
2788 </div>
2789
2790 <h2 id="vigilante-section-tools-main" class="vigilante-tools-heading"><?php esc_html_e( 'Tools', 'vigilante' ); ?></h2>
2791
2792 <?php
2793 // Warn that during Under Attack mode the export/import operate against
2794 // the temporary hardened config and any imported changes will be
2795 // reverted when the mode ends.
2796 $ua_status = get_option( Vigilante_Under_Attack::OPTION_NAME, array() );
2797 if ( ! empty( $ua_status['active'] ) ) :
2798 ?>
2799 <div class="vigilante-ua-tools-notice-wrap">
2800 <div class="notice notice-warning inline vigilante-ua-tools-notice">
2801 <p>
2802 <strong><?php esc_html_e( 'Under Attack mode is active.', 'vigilante' ); ?></strong>
2803 <?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' ); ?>
2804 </p>
2805 </div>
2806 </div>
2807 <?php
2808 endif;
2809 ?>
2810
2811 <div class="vigilante-tools-grid">
2812 <div class="vigilante-tool-card">
2813 <h3><?php esc_html_e( 'Export Settings', 'vigilante' ); ?></h3>
2814 <p><?php esc_html_e( 'Download your current security settings as a JSON file.', 'vigilante' ); ?></p>
2815 <button type="button" class="button vigilante-export-settings">
2816 <?php esc_html_e( 'Export Settings', 'vigilante' ); ?>
2817 </button>
2818 </div>
2819
2820 <div class="vigilante-tool-card">
2821 <h3><?php esc_html_e( 'Import Settings', 'vigilante' ); ?></h3>
2822 <p><?php esc_html_e( 'Import settings from a previously exported JSON file.', 'vigilante' ); ?></p>
2823 <input type="file" id="vigilante-import-file" accept=".json" style="display: none;">
2824 <button type="button" class="button vigilante-import-settings">
2825 <?php esc_html_e( 'Import Settings', 'vigilante' ); ?>
2826 </button>
2827 </div>
2828
2829 <div class="vigilante-tool-card">
2830 <h3><?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?></h3>
2831 <p><?php esc_html_e( 'Reset all the Vigilant security settings to default values. Your IP lists, custom login address, two-factor setup, scan exclusions and extra alert recipients are kept.', 'vigilante' ); ?></p>
2832 <button type="button" class="button vigilante-reset-settings" style="color: #a00;">
2833 <?php esc_html_e( 'Reset All Settings', 'vigilante' ); ?>
2834 </button>
2835 </div>
2836
2837 <div class="vigilante-tool-card">
2838 <h3><?php esc_html_e( 'Download Config Backup', 'vigilante' ); ?></h3>
2839 <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>
2840 <?php if ( $this->shared_files_locked() ) : ?>
2841 <p class="description"><?php esc_html_e( 'Both files belong to the whole network, and wp-config.php carries the database credentials and the authentication salts of every site. The copy is taken from the main site.', 'vigilante' ); ?></p>
2842 <?php else : ?>
2843 <button type="button" class="button vigilante-create-backup">
2844 <?php esc_html_e( 'Download Backup', 'vigilante' ); ?>
2845 </button>
2846 <?php endif; ?>
2847 </div>
2848
2849 <?php if ( ! $this->shared_files_locked() ) : ?>
2850
2851 <div class="vigilante-tool-card vigilante-tool-card-wide">
2852 <h3><?php esc_html_e( 'Database Backup', 'vigilante' ); ?></h3>
2853 <p><?php esc_html_e( 'Download a backup of your database as a ZIP file. Select which tables to include.', 'vigilante' ); ?></p>
2854 <button type="button" class="button vigilante-db-backup-toggle">
2855 <?php esc_html_e( 'Download Database Backup', 'vigilante' ); ?>
2856 </button>
2857
2858 <div class="vigilante-db-backup-panel" style="display: none;">
2859 <div class="vigilante-db-tables-loading">
2860 <span class="spinner is-active"></span>
2861 <?php esc_html_e( 'Loading tables...', 'vigilante' ); ?>
2862 </div>
2863
2864 <div class="vigilante-db-tables-content" style="display: none;">
2865 <div class="vigilante-db-tables-controls">
2866 <label>
2867 <input type="checkbox" id="vigilante-db-select-all" checked>
2868 <strong><?php esc_html_e( 'Select / deselect all', 'vigilante' ); ?></strong>
2869 </label>
2870 <span class="vigilante-db-tables-info"></span>
2871 </div>
2872
2873 <div class="vigilante-db-tables-group">
2874 <h4><?php esc_html_e( 'WordPress core tables', 'vigilante' ); ?></h4>
2875 <div class="vigilante-db-tables-list" id="vigilante-db-core-tables"></div>
2876 </div>
2877
2878 <div class="vigilante-db-tables-group" id="vigilante-db-other-group" style="display: none;">
2879 <h4><?php esc_html_e( 'Plugin and custom tables', 'vigilante' ); ?></h4>
2880 <div class="vigilante-db-tables-list" id="vigilante-db-other-tables"></div>
2881 </div>
2882
2883 <div class="vigilante-db-backup-actions">
2884 <button type="button" class="button button-primary vigilante-db-backup-download">
2885 <?php esc_html_e( 'Download Backup (.zip)', 'vigilante' ); ?>
2886 </button>
2887 </div>
2888 </div>
2889 </div>
2890 </div>
2891 <?php else : ?>
2892 <div class="vigilante-tool-card vigilante-tool-card-wide">
2893 <h3><?php esc_html_e( 'Database Backup', 'vigilante' ); ?></h3>
2894 <p><?php esc_html_e( 'Download a backup of your database as a ZIP file. Select which tables to include.', 'vigilante' ); ?></p>
2895 <p class="description"><?php esc_html_e( 'The database is shared by the whole network, so a backup taken here would carry every other site and all of the network users. The copy is taken from the main site.', 'vigilante' ); ?></p>
2896 </div>
2897 <?php endif; ?>
2898 </div>
2899 <?php
2900 }
2901
2902 /**
2903 * Render firewall tab
2904 */
2905 private function render_tab_firewall() {
2906 $is_disabled = $this->render_module_disabled_notice( 'firewall' );
2907 $options = $this->settings->get_section( 'firewall' );
2908 ?>
2909 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="firewall" <?php echo $is_disabled ? 'inert' : ''; ?>>
2910 <div id="vigilante-section-firewall-main" class="vigilante-settings-section">
2911 <h2>
2912 <?php esc_html_e( 'Firewall Protection', 'vigilante' ); ?>
2913 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
2914 </h2>
2915 <p><?php esc_html_e( 'PHP-based request filtering. Analyzes each request before WordPress loads.', 'vigilante' ); ?></p>
2916 <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;">
2917 <p style="margin:0;">
2918 <?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' ); ?>
2919 </p>
2920 </div>
2921
2922 <table class="form-table">
2923 <tr>
2924 <th scope="row"><?php esc_html_e( 'Block Bad Query Strings', 'vigilante' ); ?></th>
2925 <td>
2926 <label>
2927 <input type="checkbox" name="firewall[block_bad_query_strings]" value="1" <?php checked( ! empty( $options['block_bad_query_strings'] ) ); ?>>
2928 <?php esc_html_e( 'Block malicious query string patterns', 'vigilante' ); ?>
2929 </label>
2930 </td>
2931 </tr>
2932 <tr>
2933 <th scope="row"><?php esc_html_e( 'SQL Injection Protection', 'vigilante' ); ?></th>
2934 <td>
2935 <label>
2936 <input type="checkbox" name="firewall[block_sql_injection]" value="1" <?php checked( ! empty( $options['block_sql_injection'] ) ); ?>>
2937 <?php esc_html_e( 'Block SQL injection attempts', 'vigilante' ); ?>
2938 </label>
2939 </td>
2940 </tr>
2941 <tr>
2942 <th scope="row"><?php esc_html_e( 'XSS Protection', 'vigilante' ); ?></th>
2943 <td>
2944 <label>
2945 <input type="checkbox" name="firewall[block_xss_attacks]" value="1" <?php checked( ! empty( $options['block_xss_attacks'] ) ); ?>>
2946 <?php esc_html_e( 'Block cross-site scripting attacks', 'vigilante' ); ?>
2947 </label>
2948 </td>
2949 </tr>
2950 <tr>
2951 <th scope="row"><?php esc_html_e( 'File Inclusion Protection', 'vigilante' ); ?></th>
2952 <td>
2953 <label>
2954 <input type="checkbox" name="firewall[block_file_inclusion]" value="1" <?php checked( ! empty( $options['block_file_inclusion'] ) ); ?>>
2955 <?php esc_html_e( 'Block local/remote file inclusion attempts', 'vigilante' ); ?>
2956 </label>
2957 </td>
2958 </tr>
2959 <tr>
2960 <th scope="row"><?php esc_html_e( 'Directory Traversal Protection', 'vigilante' ); ?></th>
2961 <td>
2962 <label>
2963 <input type="checkbox" name="firewall[block_directory_traversal]" value="1" <?php checked( ! empty( $options['block_directory_traversal'] ) ); ?>>
2964 <?php esc_html_e( 'Block path traversal attempts', 'vigilante' ); ?>
2965 </label>
2966 </td>
2967 </tr>
2968 <tr>
2969 <th scope="row"><?php esc_html_e( 'Block Bad Bots', 'vigilante' ); ?></th>
2970 <td>
2971 <label>
2972 <input type="checkbox" name="firewall[block_bad_bots]" value="1" <?php checked( ! empty( $options['block_bad_bots'] ) ); ?>>
2973 <?php esc_html_e( 'Block known malicious bots and scanners', 'vigilante' ); ?>
2974 </label>
2975 </td>
2976 </tr>
2977 </table>
2978
2979 <h3><?php esc_html_e( 'Rate Limiting', 'vigilante' ); ?></h3>
2980 <table class="form-table">
2981 <tr>
2982 <th scope="row"><?php esc_html_e( 'Enable Rate Limiting', 'vigilante' ); ?></th>
2983 <td>
2984 <label>
2985 <input type="checkbox" name="firewall[rate_limiting][enabled]" value="1" <?php checked( ! empty( $options['rate_limiting']['enabled'] ) ); ?>>
2986 <?php esc_html_e( 'Limit requests per IP address', 'vigilante' ); ?>
2987 </label>
2988 </td>
2989 </tr>
2990 <tr>
2991 <th scope="row"><?php esc_html_e( 'Requests per Minute', 'vigilante' ); ?></th>
2992 <td>
2993 <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">
2994 <p class="description">
2995 <?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' ); ?>
2996 </p>
2997 </td>
2998 </tr>
2999 <tr>
3000 <th scope="row"><?php esc_html_e( 'Block Duration (seconds)', 'vigilante' ); ?></th>
3001 <td>
3002 <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">
3003 </td>
3004 </tr>
3005 <tr>
3006 <th scope="row"><?php esc_html_e( 'Progressive Blocking', 'vigilante' ); ?></th>
3007 <td>
3008 <label>
3009 <input type="checkbox" name="firewall[rate_limiting][progressive]" value="1" <?php checked( ! empty( $options['rate_limiting']['progressive'] ) ); ?>>
3010 <?php esc_html_e( 'Double block duration on each repeat offense', 'vigilante' ); ?>
3011 </label>
3012 <p class="description">
3013 <?php
3014 $base = absint( $options['rate_limiting']['block_duration'] ?? 300 );
3015 printf(
3016 /* translators: 1: First block duration, 2: Second, 3: Third */
3017 esc_html__( 'Example: %1$s → %2$s → %3$s and so on, up to the maximum.', 'vigilante' ),
3018 esc_html( human_time_diff( 0, $base ) ),
3019 esc_html( human_time_diff( 0, $base * 2 ) ),
3020 esc_html( human_time_diff( 0, $base * 4 ) )
3021 );
3022 ?>
3023 </p>
3024 </td>
3025 </tr>
3026 <tr>
3027 <th scope="row"><?php esc_html_e( 'Maximum Block Duration', 'vigilante' ); ?></th>
3028 <td>
3029 <select name="firewall[rate_limiting][max_block_duration]">
3030 <?php
3031 $max_options = array(
3032 3600 => __( '1 hour', 'vigilante' ),
3033 21600 => __( '6 hours', 'vigilante' ),
3034 43200 => __( '12 hours', 'vigilante' ),
3035 86400 => __( '24 hours', 'vigilante' ),
3036 604800 => __( '7 days', 'vigilante' ),
3037 );
3038 $current_max = absint( $options['rate_limiting']['max_block_duration'] ?? 86400 );
3039 foreach ( $max_options as $val => $label ) :
3040 ?>
3041 <option value="<?php echo esc_attr( $val ); ?>" <?php selected( $current_max, $val ); ?>>
3042 <?php echo esc_html( $label ); ?>
3043 </option>
3044 <?php endforeach; ?>
3045 </select>
3046 <p class="description"><?php esc_html_e( 'Upper limit for progressive blocking.', 'vigilante' ); ?></p>
3047 </td>
3048 </tr>
3049 </table>
3050
3051 <?php
3052 // Currently blocked IPs from rate limiting
3053 $active_blocks = Vigilante_Firewall::get_active_blocks();
3054 if ( ! empty( $active_blocks ) ) :
3055 ?>
3056 <div class="vigilante-settings-section vigilante-lockout-section" style="margin-top:20px;">
3057 <h3><?php esc_html_e( 'Currently Blocked IPs', 'vigilante' ); ?></h3>
3058 <table class="wp-list-table widefat fixed striped" style="max-width:800px;">
3059 <thead>
3060 <tr>
3061 <th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th>
3062 <th><?php esc_html_e( 'Blocked', 'vigilante' ); ?></th>
3063 <th><?php esc_html_e( 'Expires in', 'vigilante' ); ?></th>
3064 <th><?php esc_html_e( 'Strikes', 'vigilante' ); ?></th>
3065 <th><?php esc_html_e( 'Action', 'vigilante' ); ?></th>
3066 </tr>
3067 </thead>
3068 <tbody>
3069 <?php foreach ( $active_blocks as $blocked_ip => $block_data ) : ?>
3070 <tr>
3071 <td><code><?php echo esc_html( $blocked_ip ); ?></code></td>
3072 <td><?php echo esc_html( human_time_diff( $block_data['blocked_at'] ) . ' ' . __( 'ago', 'vigilante' ) ); ?></td>
3073 <td><?php echo esc_html( human_time_diff( time(), $block_data['expires'] ) ); ?></td>
3074 <td><?php echo esc_html( $block_data['strikes'] ?? 1 ); ?></td>
3075 <td>
3076 <button type="button" class="button button-small vigilante-unblock-firewall-ip"
3077 data-ip="<?php echo esc_attr( $blocked_ip ); ?>">
3078 <?php esc_html_e( 'Unblock', 'vigilante' ); ?>
3079 </button>
3080 </td>
3081 </tr>
3082 <?php endforeach; ?>
3083 </tbody>
3084 </table>
3085 </div>
3086 <?php endif; ?>
3087
3088 <h3><?php esc_html_e( 'IP Lists', 'vigilante' ); ?></h3>
3089 <p class="description">
3090 <?php
3091 printf(
3092 /* translators: %s: Current visitor IP address */
3093 esc_html__( 'Your current IP address: %s', 'vigilante' ),
3094 '<code>' . esc_html( $this->database->get_client_ip() ) . '</code>'
3095 );
3096 ?>
3097 </p>
3098 <table class="form-table">
3099 <tr>
3100 <th scope="row"><?php esc_html_e( 'Visitor IP detection', 'vigilante' ); ?></th>
3101 <td>
3102 <?php $proxy_header = $options['trusted_proxy_header'] ?? ''; ?>
3103 <select name="firewall[trusted_proxy_header]">
3104 <option value="" <?php selected( $proxy_header, '' ); ?>><?php esc_html_e( 'Direct connection, only REMOTE_ADDR (recommended)', 'vigilante' ); ?></option>
3105 <option value="cf-connecting-ip" <?php selected( $proxy_header, 'cf-connecting-ip' ); ?>><?php esc_html_e( 'Behind Cloudflare (CF-Connecting-IP)', 'vigilante' ); ?></option>
3106 <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>
3107 <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>
3108 </select>
3109 <p class="description">
3110 <?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' ); ?>
3111 </p>
3112 </td>
3113 </tr>
3114 <tr>
3115 <th scope="row"><?php esc_html_e( 'IP Whitelist', 'vigilante' ); ?></th>
3116 <td>
3117 <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>
3118 <p class="description">
3119 <?php esc_html_e( 'One IP per line. These IPs will bypass firewall checks.', 'vigilante' ); ?>
3120 <br>
3121 <?php
3122 printf(
3123 /* translators: 1: opening <code>, 2: closing </code>. Placeholders wrap the IP, CIDR and wildcard examples. */
3124 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' ),
3125 '<code>',
3126 '</code>'
3127 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded.
3128 ?>
3129 </p>
3130 </td>
3131 </tr>
3132 <tr>
3133 <th scope="row"><?php esc_html_e( 'IP Blacklist', 'vigilante' ); ?></th>
3134 <td>
3135 <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>
3136 <p class="description">
3137 <?php esc_html_e( 'One IP per line. These IPs will be blocked immediately.', 'vigilante' ); ?>
3138 <br>
3139 <?php
3140 printf(
3141 /* translators: 1: opening <code>, 2: closing </code>. Placeholders wrap the IP, CIDR and wildcard examples. */
3142 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' ),
3143 '<code>',
3144 '</code>'
3145 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded.
3146 ?>
3147 </p>
3148 </td>
3149 </tr>
3150 </table>
3151
3152 <h3><?php esc_html_e( 'User-Agent Lists', 'vigilante' ); ?></h3>
3153 <p><?php esc_html_e( 'Partial matching: enter a keyword and any User-Agent containing it will be matched.', 'vigilante' ); ?></p>
3154 <table class="form-table">
3155 <tr>
3156 <th scope="row"><?php esc_html_e( 'User-Agent Whitelist', 'vigilante' ); ?></th>
3157 <td>
3158 <textarea name="firewall[ua_whitelist]" rows="4" class="large-text code"><?php echo esc_textarea( implode( "\n", $options['ua_whitelist'] ?? array() ) ); ?></textarea>
3159 <p class="description"><?php esc_html_e( 'One User-Agent per line. These will bypass all firewall checks. Example: ManageWP, MainWP, UptimeRobot.', 'vigilante' ); ?></p>
3160 </td>
3161 </tr>
3162 <tr>
3163 <th scope="row"><?php esc_html_e( 'User-Agent Blacklist', 'vigilante' ); ?></th>
3164 <td>
3165 <textarea name="firewall[ua_blacklist]" rows="4" class="large-text code"><?php echo esc_textarea( implode( "\n", $options['ua_blacklist'] ?? array() ) ); ?></textarea>
3166 <p class="description"><?php esc_html_e( 'One User-Agent per line. These will be blocked immediately.', 'vigilante' ); ?></p>
3167 </td>
3168 </tr>
3169 </table>
3170 </div>
3171
3172 <?php
3173 $vg_shared_locked = $this->shared_files_locked();
3174 // Paint what is actually in force, not this site's unused copy.
3175 $vg_local_options = $options;
3176 $options = $this->get_section_for_display( 'firewall' );
3177 ?>
3178 <?php $this->render_shared_files_notice(); ?>
3179 <div id="vigilante-section-firewall-server" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
3180 <h2>
3181 <?php esc_html_e( 'Server Protection', 'vigilante' ); ?>
3182 <span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span>
3183 </h2>
3184 <p><?php esc_html_e( 'Server-level rules for Apache/LiteSpeed. These rules are processed before PHP.', 'vigilante' ); ?></p>
3185
3186 <table class="form-table">
3187 <tr id="field-disable-directory-browsing">
3188 <th scope="row"><?php esc_html_e( 'Directory Browsing', 'vigilante' ); ?></th>
3189 <td>
3190 <label>
3191 <input type="checkbox" name="firewall[disable_directory_browsing]" value="1" <?php checked( ! empty( $options['disable_directory_browsing'] ) ); ?>>
3192 <?php esc_html_e( 'Disable directory listing (Options -Indexes)', 'vigilante' ); ?>
3193 </label>
3194 </td>
3195 </tr>
3196 <tr>
3197 <th scope="row"><?php esc_html_e( 'Protect wp-config.php', 'vigilante' ); ?></th>
3198 <td>
3199 <label>
3200 <input type="checkbox" name="firewall[protect_wp_config]" value="1" <?php checked( ! empty( $options['protect_wp_config'] ) ); ?>>
3201 <?php esc_html_e( 'Block direct HTTP access to wp-config.php', 'vigilante' ); ?>
3202 </label>
3203 </td>
3204 </tr>
3205 <tr id="field-protect-wp-cron">
3206 <th scope="row"><?php esc_html_e( 'Protect wp-cron.php', 'vigilante' ); ?></th>
3207 <td>
3208 <label>
3209 <input type="checkbox" name="firewall[protect_wp_cron]" value="1" <?php checked( ! empty( $options['protect_wp_cron'] ) ); ?>>
3210 <?php esc_html_e( 'Block direct HTTP access to wp-cron.php (prevents cron-spam DoS abuse)', 'vigilante' ); ?>
3211 </label>
3212 <p class="description"><?php
3213 printf(
3214 /* translators: 1: opening <strong>, 2: closing </strong> */
3215 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' ),
3216 '<strong>',
3217 '</strong>',
3218 '<code>',
3219 '</code>'
3220 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded.
3221 ?></p>
3222 </td>
3223 </tr>
3224 <tr>
3225 <th scope="row"><?php esc_html_e( 'Protect wp-includes', 'vigilante' ); ?></th>
3226 <td>
3227 <label>
3228 <input type="checkbox" name="firewall[protect_wp_includes]" value="1" <?php checked( ! empty( $options['protect_wp_includes'] ) ); ?>>
3229 <?php esc_html_e( 'Block direct access to PHP files in wp-includes', 'vigilante' ); ?>
3230 </label>
3231 </td>
3232 </tr>
3233 <tr>
3234 <th scope="row"><?php esc_html_e( 'PHP in Uploads', 'vigilante' ); ?></th>
3235 <td>
3236 <label>
3237 <input type="checkbox" name="firewall[protect_uploads_php]" value="1" <?php checked( ! empty( $options['protect_uploads_php'] ) ); ?>>
3238 <?php esc_html_e( 'Block PHP execution in wp-content/uploads', 'vigilante' ); ?>
3239 </label>
3240 </td>
3241 </tr>
3242 <tr>
3243 <th scope="row"><?php esc_html_e( 'Sensitive Files', 'vigilante' ); ?></th>
3244 <td>
3245 <label>
3246 <input type="checkbox" name="firewall[protect_sensitive_files]" value="1" <?php checked( ! empty( $options['protect_sensitive_files'] ) ); ?>>
3247 <?php esc_html_e( 'Block access to .sql, .bak, .log, .ini, readme.html, license.txt, licencia.txt', 'vigilante' ); ?>
3248 </label>
3249 </td>
3250 </tr>
3251 <tr>
3252 <th scope="row"><?php esc_html_e( 'Limit HTTP Methods', 'vigilante' ); ?></th>
3253 <td>
3254 <label>
3255 <input type="checkbox" name="firewall[limit_http_methods]" value="1" <?php checked( ! empty( $options['limit_http_methods'] ) ); ?>>
3256 <?php esc_html_e( 'Allow only GET, POST, HEAD (blocks PUT, DELETE, TRACE, etc.)', 'vigilante' ); ?>
3257 </label>
3258 </td>
3259 </tr>
3260 </table>
3261 </div>
3262 <?php $options = $vg_local_options; ?>
3263
3264 <p class="submit vigilante-submit-buttons">
3265 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
3266 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
3267 </button>
3268 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
3269 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
3270 </button>
3271 </p>
3272 </form>
3273 <?php
3274 }
3275
3276 /**
3277 * Render login security tab
3278 */
3279 private function render_tab_login() {
3280 $is_disabled = $this->render_module_disabled_notice( 'login_security' );
3281 $options = $this->settings->get_section( 'login_security' );
3282 $lockouts = $this->database->get_active_lockouts();
3283 ?>
3284 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="login_security" <?php echo $is_disabled ? 'inert' : ''; ?>>
3285 <div id="vigilante-section-login-main" class="vigilante-settings-section">
3286 <h2>
3287 <?php esc_html_e( 'Login Protection', 'vigilante' ); ?>
3288 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
3289 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
3290 </h2>
3291 <p><?php esc_html_e( 'Brute force protection and WordPress login hardening.', 'vigilante' ); ?></p>
3292
3293 <table class="form-table">
3294 <tr id="field-max-attempts">
3295 <th scope="row"><?php esc_html_e( 'Max Login Attempts', 'vigilante' ); ?></th>
3296 <td>
3297 <input type="number" name="login_security[max_attempts]" value="<?php echo esc_attr( $options['max_attempts'] ?? 5 ); ?>" min="1" max="20" class="small-text">
3298 <p class="description"><?php esc_html_e( 'Number of failed attempts before lockout.', 'vigilante' ); ?></p>
3299 </td>
3300 </tr>
3301 <tr>
3302 <th scope="row"><?php esc_html_e( 'Lockout Duration', 'vigilante' ); ?></th>
3303 <td>
3304 <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">
3305 <?php esc_html_e( 'minutes', 'vigilante' ); ?>
3306 </td>
3307 </tr>
3308 <tr>
3309 <th scope="row"><?php esc_html_e( 'Progressive Lockout', 'vigilante' ); ?></th>
3310 <td>
3311 <label>
3312 <input type="checkbox" name="login_security[lockout_increment]" value="1" <?php checked( ! empty( $options['lockout_increment'] ) ); ?>>
3313 <?php esc_html_e( 'Double lockout duration for repeat offenders', 'vigilante' ); ?>
3314 </label>
3315 </td>
3316 </tr>
3317 <tr>
3318 <th scope="row"><?php esc_html_e( 'Hide Login Errors', 'vigilante' ); ?></th>
3319 <td>
3320 <label>
3321 <input type="checkbox" name="login_security[hide_login_errors]" value="1" <?php checked( ! empty( $options['hide_login_errors'] ) ); ?>>
3322 <?php esc_html_e( 'Show generic error message instead of specific errors', 'vigilante' ); ?>
3323 </label>
3324 </td>
3325 </tr>
3326 <tr>
3327 <th scope="row"><?php esc_html_e( 'Disable Application Passwords', 'vigilante' ); ?></th>
3328 <td>
3329 <label>
3330 <input type="checkbox" name="login_security[disable_application_passwords]" value="1" <?php checked( ! empty( $options['disable_application_passwords'] ) ); ?>>
3331 <?php esc_html_e( 'Disable WordPress application passwords feature', 'vigilante' ); ?>
3332 </label>
3333 </td>
3334 </tr>
3335 </table>
3336
3337 <h3><?php esc_html_e( 'Custom Login URL', 'vigilante' ); ?></h3>
3338 <table class="form-table">
3339 <tr id="field-custom-login-url">
3340 <th scope="row"><?php esc_html_e( 'Login URL Slug', 'vigilante' ); ?></th>
3341 <td>
3342 <code><?php echo esc_url( home_url( '/' ) ); ?></code>
3343 <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' ); ?>">
3344 <p class="description">
3345 <?php esc_html_e( '&#9432; Leave empty to use default wp-login.php. Use only lowercase letters, numbers and hyphens.', 'vigilante' ); ?>
3346 </p>
3347 <div class="vigilante-login-url-preview" <?php echo empty( $options['custom_login_url'] ) ? 'style="display:none;"' : ''; ?>>
3348 <p class="description">
3349 <strong><?php esc_html_e( 'Your login URL:', 'vigilante' ); ?></strong>
3350 <code class="vigilante-login-url-display"><?php echo esc_url( home_url( sanitize_title( $options['custom_login_url'] ?? '' ) . '/' ) ); ?></code>
3351 </p>
3352 <p class="description">
3353 <?php esc_html_e( 'Direct access to wp-login.php and wp-admin will return a 404 error for non-logged users.', 'vigilante' ); ?>
3354 </p>
3355 </div>
3356 </td>
3357 </tr>
3358 </table>
3359
3360 <div class="vigilante-login-url-notify-wrapper <?php echo empty( $options['custom_login_url'] ) ? 'vigilante-login-url-notify-disabled' : ''; ?>">
3361 <table class="form-table">
3362 <tr>
3363 <th scope="row"><?php esc_html_e( 'Notify users', 'vigilante' ); ?></th>
3364 <td>
3365 <label>
3366 <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 ); ?>>
3367 <?php esc_html_e( 'Notify affected users when the login URL changes', 'vigilante' ); ?>
3368 </label>
3369 <div class="vigilante-login-url-send-notification" style="margin-top:12px;">
3370 <button type="button" id="vigilante_notify_login_url" class="button">
3371 <?php esc_html_e( 'Send notification now', 'vigilante' ); ?>
3372 </button>
3373 <span class="vigilante-login-url-notify-status"></span>
3374 <p class="description"><?php esc_html_e( 'Sends the new URL to administrators, editors, authors, and contributors.', 'vigilante' ); ?></p>
3375 </div>
3376 </td>
3377 </tr>
3378 </table>
3379 </div>
3380
3381 <?php $this->render_2fa_settings( $options ); ?>
3382
3383 <h3><?php esc_html_e( 'Notifications', 'vigilante' ); ?></h3>
3384 <table class="form-table">
3385 <tr>
3386 <th scope="row"><?php esc_html_e( 'Notify on Lockout', 'vigilante' ); ?></th>
3387 <td>
3388 <label>
3389 <input type="checkbox" name="login_security[notify_on_lockout]" value="1" <?php checked( ! empty( $options['notify_on_lockout'] ) ); ?>>
3390 <?php esc_html_e( 'Send email when an IP is locked out', 'vigilante' ); ?>
3391 </label>
3392 </td>
3393 </tr>
3394 <tr>
3395 <th scope="row"><?php esc_html_e( 'Notify on Admin Login', 'vigilante' ); ?></th>
3396 <td>
3397 <label>
3398 <input type="checkbox" name="login_security[notify_on_admin_login]" value="1" <?php checked( ! empty( $options['notify_on_admin_login'] ) ); ?>>
3399 <?php esc_html_e( 'Send email when an administrator logs in', 'vigilante' ); ?>
3400 </label>
3401 </td>
3402 </tr>
3403 </table>
3404
3405 <p class="description">
3406 <?php
3407 printf(
3408 /* translators: %s: Link to notification settings */
3409 esc_html__( '&#9432; Notifications are sent to the recipients configured in %s.', 'vigilante' ),
3410 '<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>'
3411 );
3412 ?>
3413 </p>
3414 </div>
3415
3416 <p class="submit vigilante-submit-buttons">
3417 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
3418 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
3419 </button>
3420 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
3421 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
3422 </button>
3423 </p>
3424 </form>
3425
3426 <?php $this->render_lockout_info_section( $options, $lockouts ); ?>
3427 <?php
3428 }
3429
3430 /**
3431 * Render lockout information and blocked IPs section
3432 *
3433 * @param array $options Login security options.
3434 * @param array $lockouts Currently locked out IPs.
3435 */
3436 private function render_lockout_info_section( $options, $lockouts ) {
3437 $max_attempts = absint( $options['max_attempts'] ?? 5 );
3438 $lockout_duration = absint( $options['lockout_duration'] ?? 1800 );
3439 $lockout_increment = ! empty( $options['lockout_increment'] );
3440 $max_lockout = absint( $options['max_lockout_duration'] ?? 86400 );
3441 $two_factor = $options['two_factor'] ?? array();
3442 $two_factor_enabled = ! empty( $two_factor['enabled'] );
3443 ?>
3444 <div class="vigilante-settings-section vigilante-lockout-section">
3445 <h2><?php esc_html_e( 'Login Protection Status', 'vigilante' ); ?></h2>
3446
3447 <table class="form-table">
3448 <tr>
3449 <th scope="row"><?php esc_html_e( 'Current settings', 'vigilante' ); ?></th>
3450 <td>
3451 <?php
3452 printf(
3453 /* translators: 1: Maximum login attempts, 2: Lockout duration in minutes */
3454 esc_html__( 'After %1$d failed login attempts, the IP address is blocked for %2$d minutes.', 'vigilante' ),
3455 absint( $max_attempts ),
3456 absint( ceil( $lockout_duration / 60 ) )
3457 );
3458
3459 if ( $lockout_increment ) {
3460 echo '<br>';
3461 printf(
3462 /* translators: %d: Maximum lockout duration in hours */
3463 esc_html__( 'Progressive lockout enabled (max: %d hours).', 'vigilante' ),
3464 absint( ceil( $max_lockout / 3600 ) )
3465 );
3466 }
3467
3468 if ( $two_factor_enabled ) {
3469 echo '<br>';
3470 esc_html_e( 'Failed 2FA codes also count toward the lockout limit.', 'vigilante' );
3471 }
3472 ?>
3473 </td>
3474 </tr>
3475 <tr>
3476 <th scope="row"><?php esc_html_e( 'Blocked IPs', 'vigilante' ); ?></th>
3477 <td>
3478 <?php if ( ! empty( $lockouts ) ) : ?>
3479 <div class="vigilante-paginated-section">
3480 <div class="vigilante-fi-pagination-wrap"></div>
3481 <table class="wp-list-table widefat fixed striped vigilante-fi-paginated">
3482 <thead>
3483 <tr>
3484 <th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th>
3485 <th><?php esc_html_e( 'Attempts', 'vigilante' ); ?></th>
3486 <th><?php esc_html_e( 'Blocked Until', 'vigilante' ); ?></th>
3487 <th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
3488 </tr>
3489 </thead>
3490 <tbody>
3491 <?php foreach ( $lockouts as $lockout ) :
3492 $lockout_time = strtotime( $lockout->locked_until );
3493 $remaining_seconds = $lockout_time - time();
3494 $remaining_text = $this->format_remaining_time( $remaining_seconds );
3495 ?>
3496 <tr>
3497 <td><code><?php echo esc_html( $lockout->ip_address ); ?></code></td>
3498 <td><?php echo esc_html( $lockout->attempts ); ?></td>
3499 <td>
3500 <?php echo esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $lockout_time ) ); ?>
3501 <br>
3502 <small class="description">
3503 <?php
3504 printf(
3505 /* translators: %s: Remaining time */
3506 esc_html__( '%s remaining', 'vigilante' ),
3507 esc_html( $remaining_text )
3508 );
3509 ?>
3510 </small>
3511 </td>
3512 <td>
3513 <button type="button" class="button button-small vigilante-clear-lockout" data-ip="<?php echo esc_attr( $lockout->ip_address ); ?>">
3514 <?php esc_html_e( 'Unblock', 'vigilante' ); ?>
3515 </button>
3516 </td>
3517 </tr>
3518 <?php endforeach; ?>
3519 </tbody>
3520 </table>
3521 </div>
3522 <p class="description" style="margin-top: 10px;">
3523 <button type="button" class="button vigilante-clear-all-lockouts">
3524 <?php esc_html_e( 'Unblock All IPs', 'vigilante' ); ?>
3525 </button>
3526 <span style="margin-left: 10px;">
3527 <?php
3528 printf(
3529 /* translators: %d: Number of blocked IPs */
3530 esc_html( _n( '%d IP currently blocked', '%d IPs currently blocked', count( $lockouts ), 'vigilante' ) ),
3531 count( $lockouts )
3532 );
3533 ?>
3534 </span>
3535 </p>
3536 <?php else : ?>
3537 <span class="dashicons dashicons-yes-alt" style="color: #00a32a; font-size: 20px; width: 20px; height: 20px; vertical-align: middle;"></span>
3538 <span style="vertical-align: middle; margin-left: 5px;"><?php esc_html_e( 'No IPs are currently blocked. All clear!', 'vigilante' ); ?></span>
3539 <?php endif; ?>
3540 </td>
3541 </tr>
3542 </table>
3543 </div>
3544 <?php
3545 }
3546
3547 /**
3548 * Format remaining lockout time in human readable format
3549 *
3550 * @param int $seconds Remaining seconds.
3551 * @return string Formatted time string.
3552 */
3553 private function format_remaining_time( $seconds ) {
3554 if ( $seconds <= 0 ) {
3555 return __( 'expired', 'vigilante' );
3556 }
3557
3558 if ( $seconds < 60 ) {
3559 return sprintf(
3560 /* translators: %d: Number of seconds */
3561 _n( '%d second', '%d seconds', $seconds, 'vigilante' ),
3562 $seconds
3563 );
3564 }
3565
3566 if ( $seconds < 3600 ) {
3567 $minutes = ceil( $seconds / 60 );
3568 return sprintf(
3569 /* translators: %d: Number of minutes */
3570 _n( '%d minute', '%d minutes', $minutes, 'vigilante' ),
3571 $minutes
3572 );
3573 }
3574
3575 $hours = floor( $seconds / 3600 );
3576 $remaining_minutes = ceil( ( $seconds % 3600 ) / 60 );
3577
3578 if ( $remaining_minutes > 0 ) {
3579 return sprintf(
3580 /* translators: 1: Number of hours, 2: Number of minutes */
3581 __( '%1$d hours %2$d minutes', 'vigilante' ),
3582 $hours,
3583 $remaining_minutes
3584 );
3585 }
3586
3587 return sprintf(
3588 /* translators: %d: Number of hours */
3589 _n( '%d hour', '%d hours', $hours, 'vigilante' ),
3590 $hours
3591 );
3592 }
3593
3594 /**
3595 * Render 2FA settings section
3596 *
3597 * @param array $options Login security options.
3598 */
3599 private function render_2fa_settings( $options ) {
3600 $two_factor = $options['two_factor'] ?? array();
3601 $all_roles = wp_roles()->roles;
3602 $enforced = $two_factor['enforced_roles'] ?? array( 'administrator', 'editor' );
3603 $excluded = $two_factor['excluded_users'] ?? array();
3604 $method = $two_factor['method'] ?? 'email';
3605 $grace_days = $two_factor['grace_period_days'] ?? 3;
3606 ?>
3607 <h3>
3608 <?php esc_html_e( 'Two-Factor Authentication (2FA)', 'vigilante' ); ?>
3609 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
3610 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
3611 </h3>
3612 <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>
3613
3614 <table class="form-table">
3615 <tr>
3616 <th scope="row"><?php esc_html_e( 'Enable 2FA', 'vigilante' ); ?></th>
3617 <td>
3618 <label>
3619 <input type="checkbox" name="login_security[two_factor][enabled]" id="vigilante_2fa_enabled" value="1" <?php checked( ! empty( $two_factor['enabled'] ) ); ?>>
3620 <?php esc_html_e( 'Enable two-factor authentication', 'vigilante' ); ?>
3621 </label>
3622 </td>
3623 </tr>
3624 </table>
3625
3626 <div class="vigilante-2fa-settings-wrapper <?php echo empty( $two_factor['enabled'] ) ? 'vigilante-2fa-settings-disabled' : ''; ?>">
3627 <table class="form-table">
3628 <tr>
3629 <th scope="row"><?php esc_html_e( 'Verification method', 'vigilante' ); ?></th>
3630 <td>
3631 <fieldset class="vigilante-2fa-method-selector">
3632 <label class="vigilante-2fa-method-option <?php echo 'email' === $method ? 'selected' : ''; ?>">
3633 <input type="radio" name="login_security[two_factor][method]" value="email" <?php checked( $method, 'email' ); ?>>
3634 <span class="dashicons dashicons-email"></span>
3635 <span class="method-info">
3636 <strong><?php esc_html_e( 'Email code', 'vigilante' ); ?></strong>
3637 <span><?php esc_html_e( 'Users receive a 6-digit code via email after entering their password.', 'vigilante' ); ?></span>
3638 </span>
3639 </label>
3640 <label class="vigilante-2fa-method-option <?php echo 'totp' === $method ? 'selected' : ''; ?>">
3641 <input type="radio" name="login_security[two_factor][method]" value="totp" <?php checked( $method, 'totp' ); ?>>
3642 <span class="dashicons dashicons-smartphone"></span>
3643 <span class="method-info">
3644 <strong><?php esc_html_e( 'Authenticator app (TOTP)', 'vigilante' ); ?></strong>
3645 <span><?php esc_html_e( 'Users verify with a time-based code from Google Authenticator, Authy, etc.', 'vigilante' ); ?></span>
3646 </span>
3647 </label>
3648 </fieldset>
3649 </td>
3650 </tr>
3651 <tr>
3652 <th scope="row"><?php esc_html_e( 'Enforce for roles', 'vigilante' ); ?></th>
3653 <td>
3654 <div class="vigilante-2fa-roles">
3655 <?php foreach ( $all_roles as $role_slug => $role_data ) :
3656 $user_count = count( get_users( array( 'role' => $role_slug, 'fields' => 'ID' ) ) );
3657 ?>
3658 <label>
3659 <input type="checkbox"
3660 name="login_security[two_factor][enforced_roles][]"
3661 value="<?php echo esc_attr( $role_slug ); ?>"
3662 <?php checked( in_array( $role_slug, $enforced, true ) ); ?>>
3663 <span class="role-name"><?php echo esc_html( translate_user_role( $role_data['name'] ) ); ?></span>
3664 <span class="role-count">(<?php echo esc_html( $user_count ); ?>)</span>
3665 </label>
3666 <?php endforeach; ?>
3667 </div>
3668 <p class="description"><?php esc_html_e( 'Users with these roles will be required to verify with the selected method.', 'vigilante' ); ?></p>
3669 </td>
3670 </tr>
3671 <tr>
3672 <th scope="row"><?php esc_html_e( 'Exclude specific users', 'vigilante' ); ?></th>
3673 <td>
3674 <div class="vigilante-2fa-user-search-container">
3675 <div class="vigilante-2fa-user-search">
3676 <span class="search-icon"></span>
3677 <input type="text"
3678 id="vigilante_2fa_user_search"
3679 placeholder="<?php esc_attr_e( 'Search users by name or email...', 'vigilante' ); ?>"
3680 autocomplete="off">
3681 <div class="vigilante-2fa-search-results"></div>
3682 </div>
3683 <div class="vigilante-2fa-excluded-users">
3684 <?php
3685 foreach ( $excluded as $user_id ) :
3686 $user = get_user_by( 'ID', $user_id );
3687 if ( ! $user ) continue;
3688 ?>
3689 <div class="vigilante-2fa-excluded-user" data-user-id="<?php echo esc_attr( $user_id ); ?>">
3690 <span class="user-display"><?php echo esc_html( $user->display_name . ' (' . $user->user_email . ')' ); ?></span>
3691 <button type="button" class="remove-user" aria-label="<?php esc_attr_e( 'Remove', 'vigilante' ); ?>">&times;</button>
3692 <input type="hidden" name="login_security[two_factor][excluded_users][]" value="<?php echo esc_attr( $user_id ); ?>">
3693 </div>
3694 <?php endforeach; ?>
3695 </div>
3696 </div>
3697 <p class="description"><?php esc_html_e( 'These users will not be required to use 2FA regardless of their role.', 'vigilante' ); ?></p>
3698 </td>
3699 </tr>
3700
3701 <!-- Remember device option -->
3702 <tr>
3703 <th scope="row"><?php esc_html_e( 'Remember device', 'vigilante' ); ?></th>
3704 <td>
3705 <label>
3706 <input type="checkbox" name="login_security[two_factor][allow_remember_device]" value="1" <?php checked( ! empty( $two_factor['allow_remember_device'] ) ); ?>>
3707 <?php esc_html_e( 'Allow users to skip 2FA verification on trusted devices', 'vigilante' ); ?>
3708 </label>
3709 <p class="description">
3710 <?php
3711 printf(
3712 /* translators: %d: Number of days */
3713 esc_html__( 'When enabled, users can check "Remember this device" on the verification screen to skip 2FA for %d days.', 'vigilante' ),
3714 absint( $two_factor['remember_device_days'] ?? 30 )
3715 );
3716 ?>
3717 </p>
3718 </td>
3719 </tr>
3720
3721 <!-- TOTP-specific: Grace period -->
3722 <tr class="vigilante-2fa-totp-only" <?php echo 'totp' !== $method ? 'style="display:none;"' : ''; ?>>
3723 <th scope="row"><?php esc_html_e( 'Grace period', 'vigilante' ); ?></th>
3724 <td>
3725 <input type="number"
3726 name="login_security[two_factor][grace_period_days]"
3727 value="<?php echo esc_attr( $grace_days ); ?>"
3728 min="0" max="30" class="small-text">
3729 <?php esc_html_e( 'days', 'vigilante' ); ?>
3730 <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>
3731 </td>
3732 </tr>
3733
3734 <!-- Email-specific: Sender name -->
3735 <tr class="vigilante-2fa-email-only" <?php echo 'email' !== $method ? 'style="display:none;"' : ''; ?>>
3736 <th scope="row"><?php esc_html_e( 'Email sender name', 'vigilante' ); ?></th>
3737 <td>
3738 <input type="text"
3739 name="login_security[two_factor][email_from_name]"
3740 value="<?php echo esc_attr( $two_factor['email_from_name'] ?? '' ); ?>"
3741 class="regular-text vigilante-2fa-email-from"
3742 placeholder="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?>">
3743 <p class="description"><?php esc_html_e( 'Name shown in verification emails. Leave empty to use site name.', 'vigilante' ); ?></p>
3744 </td>
3745 </tr>
3746
3747 <!-- TOTP-specific: Reset users -->
3748 <tr class="vigilante-2fa-totp-only" <?php echo 'totp' !== $method ? 'style="display:none;"' : ''; ?>>
3749 <th scope="row"><?php esc_html_e( 'Reset user TOTP', 'vigilante' ); ?></th>
3750 <td>
3751 <div class="vigilante-totp-reset-container">
3752 <div class="vigilante-2fa-user-search">
3753 <span class="search-icon"></span>
3754 <input type="text"
3755 id="vigilante_totp_reset_search"
3756 placeholder="<?php esc_attr_e( 'Search users with TOTP configured...', 'vigilante' ); ?>"
3757 autocomplete="off">
3758 <div class="vigilante-totp-reset-results"></div>
3759 </div>
3760 <div class="vigilante-totp-reset-selected"></div>
3761 <button type="button" id="vigilante_totp_reset_btn" class="button" style="display:none;">
3762 <?php esc_html_e( 'Reset selected', 'vigilante' ); ?>
3763 </button>
3764 <span class="vigilante-totp-reset-status"></span>
3765 </div>
3766 <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>
3767 </td>
3768 </tr>
3769 </table>
3770
3771 <h3><?php esc_html_e( 'User notification', 'vigilante' ); ?></h3>
3772 <table class="form-table">
3773 <tr>
3774 <th scope="row"><?php esc_html_e( 'Notify on enable', 'vigilante' ); ?></th>
3775 <td>
3776 <label>
3777 <input type="checkbox" name="login_security[two_factor][notify_on_enable]" value="1" <?php checked( $two_factor['notify_on_enable'] ?? true ); ?>>
3778 <?php esc_html_e( 'Send notification email to affected users when 2FA is enabled', 'vigilante' ); ?>
3779 </label>
3780
3781 <div class="vigilante-2fa-notification-options">
3782 <label>
3783 <input type="radio" name="vigilante_2fa_notify_mode" value="all" checked>
3784 <?php esc_html_e( 'Send to all affected users', 'vigilante' ); ?>
3785 </label>
3786 <label>
3787 <input type="radio" name="vigilante_2fa_notify_mode" value="new">
3788 <?php esc_html_e( 'Send only to users not previously notified', 'vigilante' ); ?>
3789 </label>
3790 </div>
3791
3792 <div class="vigilante-2fa-send-notification">
3793 <button type="button" id="vigilante_2fa_send_notification" class="button">
3794 <?php esc_html_e( 'Send notification now', 'vigilante' ); ?>
3795 </button>
3796 <span class="vigilante-2fa-notification-status"></span>
3797 </div>
3798 </td>
3799 </tr>
3800 </table>
3801 </div>
3802 <?php
3803 }
3804
3805 /**
3806 * Render security headers tab
3807 */
3808 private function render_tab_headers() {
3809 $is_disabled = $this->render_module_disabled_notice( 'security_headers' );
3810 // Every setting on this tab ends up in .htaccess, so on a subsite the
3811 // whole tab is somebody else's, values included.
3812 $vg_shared_locked = $this->shared_files_locked();
3813 $options = $this->get_section_for_display( 'security_headers' );
3814 ?>
3815 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="security_headers" <?php echo $is_disabled ? 'inert' : ''; ?>>
3816 <?php $this->render_shared_files_notice(); ?>
3817 <div id="vigilante-section-headers-main" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
3818 <h2>
3819 <?php esc_html_e( 'Security Headers', 'vigilante' ); ?>
3820 <span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span>
3821 </h2>
3822 <p><?php esc_html_e( 'HTTP headers sent with every response via .htaccess (mod_headers).', 'vigilante' ); ?></p>
3823
3824 <table class="form-table">
3825 <tr>
3826 <th scope="row"><?php esc_html_e( 'X-Frame-Options', 'vigilante' ); ?></th>
3827 <td>
3828 <select name="security_headers[x_frame_options]">
3829 <option value="" <?php selected( empty( $options['x_frame_options'] ) ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option>
3830 <option value="SAMEORIGIN" <?php selected( $options['x_frame_options'] ?? '', 'SAMEORIGIN' ); ?>>SAMEORIGIN</option>
3831 <option value="DENY" <?php selected( $options['x_frame_options'] ?? '', 'DENY' ); ?>>DENY</option>
3832 </select>
3833 <p class="description"><?php esc_html_e( '&#9432; Prevents clickjacking attacks. Also sets CSP frame-ancestors automatically.', 'vigilante' ); ?></p>
3834 </td>
3835 </tr>
3836 <tr>
3837 <th scope="row"><?php esc_html_e( 'X-Content-Type-Options', 'vigilante' ); ?></th>
3838 <td>
3839 <label>
3840 <input type="checkbox" name="security_headers[x_content_type_options]" value="1" <?php checked( ! empty( $options['x_content_type_options'] ) ); ?>>
3841 <?php esc_html_e( 'Add nosniff header to prevent MIME type sniffing', 'vigilante' ); ?>
3842 </label>
3843 </td>
3844 </tr>
3845 <tr>
3846 <th scope="row"><?php esc_html_e( 'Referrer-Policy', 'vigilante' ); ?></th>
3847 <td>
3848 <select name="security_headers[referrer_policy]">
3849 <option value="" <?php selected( empty( $options['referrer_policy'] ) ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option>
3850 <option value="no-referrer" <?php selected( $options['referrer_policy'] ?? '', 'no-referrer' ); ?>>no-referrer</option>
3851 <option value="strict-origin-when-cross-origin" <?php selected( $options['referrer_policy'] ?? '', 'strict-origin-when-cross-origin' ); ?>>strict-origin-when-cross-origin</option>
3852 <option value="same-origin" <?php selected( $options['referrer_policy'] ?? '', 'same-origin' ); ?>>same-origin</option>
3853 </select>
3854 </td>
3855 </tr>
3856 </table>
3857
3858 <h3><?php esc_html_e( 'Content Security Policy', 'vigilante' ); ?></h3>
3859 <table class="form-table">
3860 <tr>
3861 <th scope="row"><?php esc_html_e( 'Enable CSP', 'vigilante' ); ?></th>
3862 <td>
3863 <label>
3864 <input type="checkbox" name="security_headers[csp][enabled]" value="1" <?php checked( ! empty( $options['csp']['enabled'] ) ); ?>>
3865 <?php esc_html_e( 'Enable Content Security Policy', 'vigilante' ); ?>
3866 </label>
3867 </td>
3868 </tr>
3869 <tr>
3870 <th scope="row"><?php esc_html_e( 'Report Only Mode', 'vigilante' ); ?></th>
3871 <td>
3872 <label>
3873 <input type="checkbox" name="security_headers[csp][report_only]" value="1" <?php checked( ! empty( $options['csp']['report_only'] ) ); ?>>
3874 <?php esc_html_e( 'Report violations without blocking (for testing)', 'vigilante' ); ?>
3875 </label>
3876 </td>
3877 </tr>
3878 </table>
3879
3880 <h3><?php esc_html_e( 'HTTPS', 'vigilante' ); ?></h3>
3881 <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>
3882 <table class="form-table">
3883 <tr>
3884 <th scope="row"><?php esc_html_e( 'Redirect HTTP to HTTPS', 'vigilante' ); ?></th>
3885 <td>
3886 <label>
3887 <input type="checkbox" name="security_headers[redirect_http_to_https]" value="1" <?php checked( ! empty( $options['redirect_http_to_https'] ) ); ?>>
3888 <?php esc_html_e( 'Send visitors arriving over HTTP to the HTTPS address', 'vigilante' ); ?>
3889 </label>
3890 <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>
3891 </td>
3892 </tr>
3893 <tr>
3894 <th scope="row"><?php esc_html_e( 'Fix Mixed Content', 'vigilante' ); ?></th>
3895 <td>
3896 <label>
3897 <input type="checkbox" name="security_headers[fix_mixed_content]" value="1" <?php checked( ! empty( $options['fix_mixed_content'] ) ); ?>>
3898 <?php esc_html_e( 'Rewrite this site http:// resources to https://', 'vigilante' ); ?>
3899 </label>
3900 <p class="description"><?php esc_html_e( 'Off by default. Only touches addresses of this same site, and only when the site is already served over HTTPS, so it cannot break an external resource. Useful right after moving a site to HTTPS, when old content still points at http:// addresses.', 'vigilante' ); ?></p>
3901 </td>
3902 </tr>
3903 <tr id="field-upgrade-insecure-requests">
3904 <th scope="row"><?php esc_html_e( 'Upgrade Insecure Requests', 'vigilante' ); ?></th>
3905 <td>
3906 <label>
3907 <input type="checkbox" name="security_headers[upgrade_insecure_requests]" value="1" <?php checked( ! empty( $options['upgrade_insecure_requests'] ) ); ?>>
3908 <?php esc_html_e( 'Ask browsers to upgrade every http:// request to https://', 'vigilante' ); ?>
3909 </label>
3910 <p class="description"><?php esc_html_e( '&#9888; Off by default. This one also covers resources hosted elsewhere: anything served from a domain with no HTTPS stops loading instead of loading insecurely. Turn it on once you know every external resource the site uses is available over HTTPS.', 'vigilante' ); ?></p>
3911 </td>
3912 </tr>
3913 <tr>
3914 <th scope="row"><?php esc_html_e( 'Rewrite Site Address on Activation', 'vigilante' ); ?></th>
3915 <td>
3916 <label>
3917 <input type="checkbox" name="security_headers[force_https]" value="1" <?php checked( ! empty( $options['force_https'] ) ); ?>>
3918 <?php esc_html_e( 'Change the WordPress and site addresses to https:// when the plugin is activated', 'vigilante' ); ?>
3919 </label>
3920 <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>
3921 </td>
3922 </tr>
3923 </table>
3924
3925 <h3><?php esc_html_e( 'HSTS (HTTP Strict Transport Security)', 'vigilante' ); ?></h3>
3926 <?php $vig_home_https = ( 0 === strpos( (string) get_option( 'home' ), 'https://' ) ); ?>
3927 <p class="description"><?php esc_html_e( 'Tells browsers to reach this site over HTTPS and never over HTTP, for as long as the max age below.', 'vigilante' ); ?></p>
3928 <?php if ( ! $vig_home_https ) : ?>
3929 <p class="description" style="color:#b32d2e"><strong><?php esc_html_e( 'Unavailable: the site address still starts with http://. Enabling HSTS on a site not published over HTTPS would make it unreachable in any browser that honours it.', 'vigilante' ); ?></strong></p>
3930 <?php endif; ?>
3931 <table class="form-table">
3932 <tr>
3933 <th scope="row"><?php esc_html_e( 'Enable HSTS', 'vigilante' ); ?></th>
3934 <td>
3935 <?php if ( ! $vig_home_https ) : ?>
3936 <?php /* A disabled checkbox is not submitted, and a boolean missing from the post is treated as unticked, so saving the tab would silently switch HSTS off. Carry the stored value instead. */ ?>
3937 <input type="hidden" name="security_headers[hsts][enabled]" value="<?php echo ! empty( $options['hsts']['enabled'] ) ? '1' : '0'; ?>">
3938 <?php endif; ?>
3939 <label>
3940 <input type="checkbox" name="security_headers[hsts][enabled]" value="1" <?php checked( ! empty( $options['hsts']['enabled'] ) ); ?> <?php disabled( ! $vig_home_https ); ?>>
3941 <?php esc_html_e( 'Send the Strict-Transport-Security header', 'vigilante' ); ?>
3942 </label>
3943 <p class="description"><?php esc_html_e( '&#9888; Hard to undo: browsers remember it for the whole max age even if you turn it off later, so a site that loses its certificate stays unreachable until it expires. Start with a short max age.', 'vigilante' ); ?></p>
3944 </td>
3945 </tr>
3946 <tr>
3947 <th scope="row"><?php esc_html_e( 'Max Age', 'vigilante' ); ?></th>
3948 <td>
3949 <select name="security_headers[hsts][max_age]">
3950 <option value="86400" <?php selected( $options['hsts']['max_age'] ?? 31536000, 86400 ); ?>><?php esc_html_e( '1 day (testing)', 'vigilante' ); ?></option>
3951 <option value="2592000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 2592000 ); ?>><?php esc_html_e( '30 days', 'vigilante' ); ?></option>
3952 <option value="31536000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 31536000 ); ?>><?php esc_html_e( '1 year (recommended)', 'vigilante' ); ?></option>
3953 <option value="63072000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 63072000 ); ?>><?php esc_html_e( '2 years', 'vigilante' ); ?></option>
3954 </select>
3955 </td>
3956 </tr>
3957 <tr>
3958 <th scope="row"><?php esc_html_e( 'Include Subdomains', 'vigilante' ); ?></th>
3959 <td>
3960 <label>
3961 <input type="checkbox" name="security_headers[hsts][include_subdomains]" value="1" <?php checked( ! empty( $options['hsts']['include_subdomains'] ) ); ?>>
3962 <?php esc_html_e( 'Apply HSTS to all subdomains', 'vigilante' ); ?>
3963 </label>
3964 </td>
3965 </tr>
3966 </table>
3967
3968 <h3><?php esc_html_e( 'Server Identity', 'vigilante' ); ?></h3>
3969 <p class="description"><?php esc_html_e( 'Hide identifying information that servers expose in responses.', 'vigilante' ); ?></p>
3970 <table class="form-table">
3971 <tr>
3972 <th scope="row"><?php esc_html_e( 'Server Signature', 'vigilante' ); ?></th>
3973 <td>
3974 <label>
3975 <input type="checkbox" name="security_headers[hide_server_signature]" value="1" <?php checked( ! empty( $options['hide_server_signature'] ) ); ?>>
3976 <?php esc_html_e( 'Hide server signature (ServerSignature Off)', 'vigilante' ); ?>
3977 </label>
3978 </td>
3979 </tr>
3980 <tr>
3981 <th scope="row"><?php esc_html_e( 'Remove Fingerprinting Headers', 'vigilante' ); ?></th>
3982 <td>
3983 <label>
3984 <input type="checkbox" name="security_headers[remove_fingerprinting_headers]" value="1" <?php checked( ! empty( $options['remove_fingerprinting_headers'] ) ); ?>>
3985 <?php esc_html_e( 'Remove X-Powered-By and Server headers', 'vigilante' ); ?>
3986 </label>
3987 </td>
3988 </tr>
3989 </table>
3990 </div>
3991
3992 <p class="submit vigilante-submit-buttons">
3993 <?php if ( ! $vg_shared_locked ) : ?>
3994 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
3995 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
3996 </button>
3997 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
3998 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
3999 </button>
4000 <?php endif; ?>
4001 <?php /* Testing what the server actually sends is read-only and useful from any site of a network. */ ?>
4002 <button type="button" class="button vigilante-test-headers">
4003 <?php esc_html_e( 'Test Headers', 'vigilante' ); ?>
4004 </button>
4005 </p>
4006 </form>
4007
4008 <div id="vigilante-headers-result"></div>
4009 <?php
4010 }
4011
4012 /**
4013 * Render REST API tab
4014 */
4015 private function render_tab_rest_api() {
4016 $is_disabled = $this->render_module_disabled_notice( 'rest_api_security' );
4017 $options = $this->settings->get_section( 'rest_api_security' );
4018 ?>
4019 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="rest_api_security" <?php echo $is_disabled ? 'inert' : ''; ?>>
4020 <div id="vigilante-section-rest-api-main" class="vigilante-settings-section">
4021 <h2>
4022 <?php esc_html_e( 'REST API Security', 'vigilante' ); ?>
4023 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4024 </h2>
4025 <p><?php esc_html_e( 'Control access to WordPress REST API endpoints.', 'vigilante' ); ?></p>
4026
4027 <table class="form-table">
4028 <tr>
4029 <th scope="row"><?php esc_html_e( 'Access Mode', 'vigilante' ); ?></th>
4030 <td>
4031 <select name="rest_api_security[mode]">
4032 <option value="open" <?php selected( $options['mode'] ?? 'selective', 'open' ); ?>><?php esc_html_e( 'Open - Allow all requests', 'vigilante' ); ?></option>
4033 <option value="selective" <?php selected( $options['mode'] ?? 'selective', 'selective' ); ?>><?php esc_html_e( 'Selective - Protect sensitive endpoints', 'vigilante' ); ?></option>
4034 <option value="authenticated_only" <?php selected( $options['mode'] ?? 'selective', 'authenticated_only' ); ?>><?php esc_html_e( 'Authenticated - Require login for all', 'vigilante' ); ?></option>
4035 </select>
4036 <p class="description"><?php esc_html_e( 'Selective mode is recommended.', 'vigilante' ); ?></p>
4037 </td>
4038 </tr>
4039 <tr id="field-block-user-enumeration">
4040 <th scope="row"><?php esc_html_e( 'Block User Enumeration', 'vigilante' ); ?></th>
4041 <td>
4042 <label>
4043 <input type="checkbox" name="rest_api_security[block_user_enumeration]" value="1" <?php checked( ! empty( $options['block_user_enumeration'] ) ); ?>>
4044 <?php esc_html_e( 'Protect /wp/v2/users endpoint for unauthenticated users', 'vigilante' ); ?>
4045 </label>
4046 </td>
4047 </tr>
4048 <tr>
4049 <th scope="row"><?php esc_html_e( 'Disable JSONP', 'vigilante' ); ?></th>
4050 <td>
4051 <label>
4052 <input type="checkbox" name="rest_api_security[disable_jsonp]" value="1" <?php checked( ! empty( $options['disable_jsonp'] ) ); ?>>
4053 <?php esc_html_e( 'Disable JSONP support in REST API', 'vigilante' ); ?>
4054 </label>
4055 </td>
4056 </tr>
4057 </table>
4058 </div>
4059
4060 <p class="submit vigilante-submit-buttons">
4061 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
4062 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
4063 </button>
4064 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
4065 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
4066 </button>
4067 </p>
4068 </form>
4069 <?php
4070 }
4071
4072 /**
4073 * Render User Security tab
4074 */
4075 private function render_tab_users() {
4076 $is_disabled = $this->render_module_disabled_notice( 'user_security' );
4077 $options = $this->settings->get_section( 'user_security' );
4078 $monitoring = $options['admin_monitoring'] ?? array();
4079 $registration = $options['registration_approval'] ?? array();
4080 $session_limits = $options['session_limits'] ?? array();
4081 $password_exp = $options['password_expiration'] ?? array();
4082 $email_verify = $options['email_verification'] ?? array();
4083 ?>
4084
4085 <!-- ============================================================
4086 SETTINGS SECTION - Single form for all configuration
4087 ============================================================ -->
4088 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="user_security" <?php echo $is_disabled ? 'inert' : ''; ?>>
4089
4090 <!-- Username & Password Protection -->
4091 <div id="vigilante-section-users-password" class="vigilante-settings-section">
4092 <h2>
4093 <?php esc_html_e( 'Username & password protection', 'vigilante' ); ?>
4094 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4095 </h2>
4096 <p><?php esc_html_e( 'Enforce secure username and password policies.', 'vigilante' ); ?></p>
4097
4098 <table class="form-table">
4099 <tr>
4100 <th scope="row"><?php esc_html_e( 'Block Insecure Usernames', 'vigilante' ); ?></th>
4101 <td>
4102 <label>
4103 <input type="checkbox" name="user_security[block_insecure_usernames]" value="1" <?php checked( ! empty( $options['block_insecure_usernames'] ) ); ?>>
4104 <?php esc_html_e( 'Prevent creation of users with common usernames (admin, administrator, etc.)', 'vigilante' ); ?>
4105 </label>
4106 </td>
4107 </tr>
4108 <?php
4109 $pw_policy = wp_parse_args(
4110 ( isset( $options['password_policy'] ) && is_array( $options['password_policy'] ) ) ? $options['password_policy'] : array(),
4111 array(
4112 'require_uppercase' => false,
4113 'require_lowercase' => false,
4114 'require_number' => false,
4115 'require_special' => false,
4116 'block_common' => true,
4117 'block_username' => true,
4118 'affected_roles' => array(),
4119 )
4120 );
4121 $pw_policy_roles = (array) $pw_policy['affected_roles'];
4122 ?>
4123 <tr>
4124 <th scope="row"><?php esc_html_e( 'Enforce Strong Passwords', 'vigilante' ); ?></th>
4125 <td>
4126 <label>
4127 <input type="checkbox" name="user_security[force_strong_passwords]" value="1" <?php checked( ! empty( $options['force_strong_passwords'] ) ); ?>>
4128 <?php esc_html_e( 'Check passwords against the requirements below when a user sets or changes one', 'vigilante' ); ?>
4129 </label>
4130 </td>
4131 </tr>
4132 <tr>
4133 <th scope="row"><?php esc_html_e( 'Minimum Password Length', 'vigilante' ); ?></th>
4134 <td>
4135 <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">
4136 <?php esc_html_e( 'characters', 'vigilante' ); ?>
4137 </td>
4138 </tr>
4139 <tr>
4140 <th scope="row"><?php esc_html_e( 'Password Requirements', 'vigilante' ); ?></th>
4141 <td>
4142 <label style="display:block;margin-bottom:5px;">
4143 <input type="checkbox" name="user_security[password_policy][require_uppercase]" value="1" <?php checked( ! empty( $pw_policy['require_uppercase'] ) ); ?>>
4144 <?php esc_html_e( 'Require an uppercase letter (A-Z)', 'vigilante' ); ?>
4145 </label>
4146 <label style="display:block;margin-bottom:5px;">
4147 <input type="checkbox" name="user_security[password_policy][require_lowercase]" value="1" <?php checked( ! empty( $pw_policy['require_lowercase'] ) ); ?>>
4148 <?php esc_html_e( 'Require a lowercase letter (a-z)', 'vigilante' ); ?>
4149 </label>
4150 <label style="display:block;margin-bottom:5px;">
4151 <input type="checkbox" name="user_security[password_policy][require_number]" value="1" <?php checked( ! empty( $pw_policy['require_number'] ) ); ?>>
4152 <?php esc_html_e( 'Require a number (0-9)', 'vigilante' ); ?>
4153 </label>
4154 <label style="display:block;margin-bottom:5px;">
4155 <input type="checkbox" name="user_security[password_policy][require_special]" value="1" <?php checked( ! empty( $pw_policy['require_special'] ) ); ?>>
4156 <?php esc_html_e( 'Require a special character (!, @, #, ...)', 'vigilante' ); ?>
4157 </label>
4158 <label style="display:block;margin-bottom:5px;">
4159 <input type="checkbox" name="user_security[password_policy][block_common]" value="1" <?php checked( ! empty( $pw_policy['block_common'] ) ); ?>>
4160 <?php esc_html_e( 'Reject well-known common passwords', 'vigilante' ); ?>
4161 </label>
4162 <label style="display:block;margin-bottom:5px;">
4163 <input type="checkbox" name="user_security[password_policy][block_username]" value="1" <?php checked( ! empty( $pw_policy['block_username'] ) ); ?>>
4164 <?php esc_html_e( 'Do not allow the username inside the password', 'vigilante' ); ?>
4165 </label>
4166 <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>
4167 </td>
4168 </tr>
4169 <tr>
4170 <th scope="row"><?php esc_html_e( 'Apply Password Rules To', 'vigilante' ); ?></th>
4171 <td>
4172 <?php foreach ( wp_roles()->get_names() as $role_slug => $role_name ) : ?>
4173 <label style="display:block;margin-bottom:5px;">
4174 <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 ) ); ?>>
4175 <?php echo esc_html( translate_user_role( $role_name ) ); ?>
4176 </label>
4177 <?php endforeach; ?>
4178 <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>
4179 </td>
4180 </tr>
4181 <tr id="field-block-author-scanning">
4182 <th scope="row"><?php esc_html_e( 'Block Author Scanning', 'vigilante' ); ?></th>
4183 <td>
4184 <label>
4185 <input type="checkbox" name="user_security[block_author_scanning]" value="1" <?php checked( ! empty( $options['block_author_scanning'] ) ); ?>>
4186 <?php esc_html_e( 'Prevent username discovery via ?author=N URLs', 'vigilante' ); ?>
4187 </label>
4188 </td>
4189 </tr>
4190 <tr>
4191 <th scope="row"><?php esc_html_e( 'Display Name Protection', 'vigilante' ); ?></th>
4192 <td>
4193 <label>
4194 <input type="checkbox" name="user_security[prevent_display_name_login_match]" value="1" <?php checked( ! empty( $options['prevent_display_name_login_match'] ) ); ?>>
4195 <?php esc_html_e( 'Prevent users from saving a display name that matches their login username', 'vigilante' ); ?>
4196 </label>
4197 <p class="description"><?php esc_html_e( 'The display name is publicly visible and should not reveal the login username.', 'vigilante' ); ?></p>
4198 </td>
4199 </tr>
4200 </table>
4201 </div>
4202
4203 <!-- Admin Monitoring -->
4204 <div id="vigilante-section-users-admin-monitoring" class="vigilante-settings-section">
4205 <h2>
4206 <?php esc_html_e( 'Admin monitoring', 'vigilante' ); ?>
4207 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4208 </h2>
4209 <p><?php esc_html_e( 'Receive email alerts when administrator accounts are modified. All events are always logged to the Security Audit.', 'vigilante' ); ?></p>
4210
4211 <table class="form-table">
4212 <tr>
4213 <th scope="row"><?php esc_html_e( 'New Administrator Alert', 'vigilante' ); ?></th>
4214 <td>
4215 <label>
4216 <input type="checkbox" name="user_security[admin_monitoring][alert_new_admin]" value="1" <?php checked( ! empty( $monitoring['alert_new_admin'] ) ); ?>>
4217 <?php esc_html_e( 'Send email alert when a new administrator account is created', 'vigilante' ); ?>
4218 </label>
4219 </td>
4220 </tr>
4221 <tr>
4222 <th scope="row"><?php esc_html_e( 'Admin Email Change Alert', 'vigilante' ); ?></th>
4223 <td>
4224 <label>
4225 <input type="checkbox" name="user_security[admin_monitoring][alert_admin_email_change]" value="1" <?php checked( ! empty( $monitoring['alert_admin_email_change'] ) ); ?>>
4226 <?php esc_html_e( 'Send email alert when an administrator email address is changed', 'vigilante' ); ?>
4227 </label>
4228 </td>
4229 </tr>
4230 <tr>
4231 <th scope="row"><?php esc_html_e( 'Permission Elevation Alert', 'vigilante' ); ?></th>
4232 <td>
4233 <label>
4234 <input type="checkbox" name="user_security[admin_monitoring][alert_permission_elevation]" value="1" <?php checked( ! empty( $monitoring['alert_permission_elevation'] ) ); ?>>
4235 <?php esc_html_e( 'Send email alert when a user is elevated to administrator role', 'vigilante' ); ?>
4236 </label>
4237 </td>
4238 </tr>
4239 <tr>
4240 <th scope="row"><?php esc_html_e( 'Admin Password Change Alert', 'vigilante' ); ?></th>
4241 <td>
4242 <label>
4243 <input type="checkbox" name="user_security[admin_monitoring][alert_admin_password_change]" value="1" <?php checked( ! empty( $monitoring['alert_admin_password_change'] ) ); ?>>
4244 <?php esc_html_e( 'Send email alert when an administrator password is changed', 'vigilante' ); ?>
4245 </label>
4246 </td>
4247 </tr>
4248 </table>
4249
4250 <p class="description">
4251 <?php
4252 printf(
4253 /* translators: %s: Link to notification settings */
4254 esc_html__( '&#9432; Notifications are sent to the recipients configured in %s.', 'vigilante' ),
4255 '<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>'
4256 );
4257 ?>
4258 </p>
4259 </div>
4260
4261 <!-- Registration Approval -->
4262 <div id="vigilante-section-users-registration" class="vigilante-settings-section">
4263 <h2>
4264 <?php esc_html_e( 'Registration approval', 'vigilante' ); ?>
4265 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4266 </h2>
4267 <p><?php esc_html_e( 'Require manual approval for new user registrations.', 'vigilante' ); ?></p>
4268
4269 <table class="form-table">
4270 <tr>
4271 <th scope="row"><?php esc_html_e( 'Enable Registration Approval', 'vigilante' ); ?></th>
4272 <td>
4273 <label>
4274 <input type="checkbox" name="user_security[registration_approval][enabled]" value="1" <?php checked( ! empty( $registration['enabled'] ) ); ?>>
4275 <?php esc_html_e( 'New users must be approved by an administrator before they can log in', 'vigilante' ); ?>
4276 </label>
4277 </td>
4278 </tr>
4279 <tr>
4280 <th scope="row"><?php esc_html_e( 'Notify Admin', 'vigilante' ); ?></th>
4281 <td>
4282 <label>
4283 <input type="checkbox" name="user_security[registration_approval][notify_admin]" value="1" <?php checked( ! empty( $registration['notify_admin'] ) ); ?>>
4284 <?php esc_html_e( 'Send email notification when a new user registers', 'vigilante' ); ?>
4285 </label>
4286 <p class="description"><?php esc_html_e( 'Disable on high-traffic sites to avoid email overload.', 'vigilante' ); ?></p>
4287 </td>
4288 </tr>
4289 <tr>
4290 <th scope="row"><?php esc_html_e( 'Auto-reject After', 'vigilante' ); ?></th>
4291 <td>
4292 <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">
4293 <?php esc_html_e( 'days (0 = never)', 'vigilante' ); ?>
4294 <p class="description"><?php esc_html_e( 'Automatically reject pending registrations after this many days.', 'vigilante' ); ?></p>
4295 </td>
4296 </tr>
4297 </table>
4298 </div>
4299
4300 <!-- Session Limits -->
4301 <div id="vigilante-section-users-sessions" class="vigilante-settings-section">
4302 <h2>
4303 <?php esc_html_e( 'Session limits', 'vigilante' ); ?>
4304 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4305 </h2>
4306 <p><?php esc_html_e( 'Limit the number of simultaneous sessions per user.', 'vigilante' ); ?></p>
4307
4308 <table class="form-table">
4309 <tr>
4310 <th scope="row"><?php esc_html_e( 'Enable Session Limits', 'vigilante' ); ?></th>
4311 <td>
4312 <label>
4313 <input type="checkbox" name="user_security[session_limits][enabled]" value="1" <?php checked( ! empty( $session_limits['enabled'] ) ); ?>>
4314 <?php esc_html_e( 'Limit the number of active sessions per user', 'vigilante' ); ?>
4315 </label>
4316 </td>
4317 </tr>
4318 <tr>
4319 <th scope="row"><?php esc_html_e( 'Maximum Sessions', 'vigilante' ); ?></th>
4320 <td>
4321 <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">
4322 <?php esc_html_e( 'sessions per user', 'vigilante' ); ?>
4323 </td>
4324 </tr>
4325 <tr>
4326 <th scope="row"><?php esc_html_e( 'When Limit Exceeded', 'vigilante' ); ?></th>
4327 <td>
4328 <select name="user_security[session_limits][behavior]">
4329 <option value="block_new" <?php selected( ( $session_limits['behavior'] ?? 'close_oldest' ), 'block_new' ); ?>><?php esc_html_e( 'Block new login', 'vigilante' ); ?></option>
4330 <option value="close_oldest" <?php selected( ( $session_limits['behavior'] ?? 'close_oldest' ), 'close_oldest' ); ?>><?php esc_html_e( 'Close oldest session', 'vigilante' ); ?></option>
4331 </select>
4332 <p class="description"><?php esc_html_e( '"Close oldest" is recommended for security - ensures attackers cannot lock out legitimate users.', 'vigilante' ); ?></p>
4333 </td>
4334 </tr>
4335 <tr>
4336 <th scope="row"><?php esc_html_e( 'Exclude Administrators', 'vigilante' ); ?></th>
4337 <td>
4338 <label>
4339 <input type="checkbox" name="user_security[session_limits][exclude_admins]" value="1" <?php checked( ! empty( $session_limits['exclude_admins'] ) ); ?>>
4340 <?php esc_html_e( 'Do not apply session limits to administrators', 'vigilante' ); ?>
4341 </label>
4342 </td>
4343 </tr>
4344 </table>
4345 </div>
4346
4347 <!-- Password Expiration -->
4348 <div id="vigilante-section-users-password-exp" class="vigilante-settings-section">
4349 <h2>
4350 <?php esc_html_e( 'Password expiration', 'vigilante' ); ?>
4351 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4352 </h2>
4353 <p><?php esc_html_e( 'Force users to change their password periodically.', 'vigilante' ); ?></p>
4354
4355 <table class="form-table">
4356 <tr>
4357 <th scope="row"><?php esc_html_e( 'Enable Password Expiration', 'vigilante' ); ?></th>
4358 <td>
4359 <label>
4360 <input type="checkbox" name="user_security[password_expiration][enabled]" value="1" <?php checked( ! empty( $password_exp['enabled'] ) ); ?>>
4361 <?php esc_html_e( 'Force password change after a set number of days', 'vigilante' ); ?>
4362 </label>
4363 </td>
4364 </tr>
4365 <tr>
4366 <th scope="row"><?php esc_html_e( 'Expire After', 'vigilante' ); ?></th>
4367 <td>
4368 <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">
4369 <?php esc_html_e( 'days', 'vigilante' ); ?>
4370 <p class="description"><?php esc_html_e( 'PCI-DSS recommends 90 days.', 'vigilante' ); ?></p>
4371 </td>
4372 </tr>
4373 <tr>
4374 <th scope="row"><?php esc_html_e( 'Warning Period', 'vigilante' ); ?></th>
4375 <td>
4376 <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">
4377 <?php esc_html_e( 'days before expiration', 'vigilante' ); ?>
4378 <p class="description"><?php esc_html_e( 'Show warning notice this many days before password expires.', 'vigilante' ); ?></p>
4379 </td>
4380 </tr>
4381 <tr>
4382 <th scope="row"><?php esc_html_e( 'Password History', 'vigilante' ); ?></th>
4383 <td>
4384 <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">
4385 <?php esc_html_e( 'passwords to remember', 'vigilante' ); ?>
4386 <p class="description"><?php esc_html_e( 'Prevent reusing recent passwords. Set to 0 to disable.', 'vigilante' ); ?></p>
4387 </td>
4388 </tr>
4389 <tr>
4390 <th scope="row"><?php esc_html_e( 'Email Reminder', 'vigilante' ); ?></th>
4391 <td>
4392 <label>
4393 <input type="checkbox" name="user_security[password_expiration][send_reminder]" value="1" <?php checked( ! empty( $password_exp['send_reminder'] ) ); ?>>
4394 <?php esc_html_e( 'Send email reminder when password is about to expire', 'vigilante' ); ?>
4395 </label>
4396 <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>
4397 </td>
4398 </tr>
4399 <tr>
4400 <th scope="row"><?php esc_html_e( 'Affected Roles', 'vigilante' ); ?></th>
4401 <td>
4402 <?php
4403 $affected_roles = $password_exp['affected_roles'] ?? array( 'administrator', 'editor' );
4404 $all_roles = wp_roles()->get_names();
4405 foreach ( $all_roles as $role_slug => $role_name ) :
4406 ?>
4407 <label style="display: block; margin-bottom: 5px;">
4408 <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 ) ); ?>>
4409 <?php echo esc_html( translate_user_role( $role_name ) ); ?>
4410 </label>
4411 <?php endforeach; ?>
4412 </td>
4413 </tr>
4414 <tr>
4415 <th scope="row"><?php esc_html_e( 'Exclude specific users', 'vigilante' ); ?></th>
4416 <td>
4417 <?php $pwexp_excluded = $password_exp['excluded_users'] ?? array(); ?>
4418 <div class="vigilante-2fa-user-search-container">
4419 <div class="vigilante-2fa-user-search">
4420 <span class="search-icon"></span>
4421 <input type="text"
4422 id="vigilante_pwexp_user_search"
4423 placeholder="<?php esc_attr_e( 'Search users by name or email...', 'vigilante' ); ?>"
4424 autocomplete="off">
4425 <div class="vigilante-pwexp-search-results"></div>
4426 </div>
4427 <div class="vigilante-pwexp-excluded-users">
4428 <?php
4429 foreach ( $pwexp_excluded as $pwexp_excluded_id ) :
4430 $excluded_user = get_user_by( 'ID', $pwexp_excluded_id );
4431 if ( ! $excluded_user ) {
4432 continue;
4433 }
4434 ?>
4435 <div class="vigilante-pwexp-excluded-user" data-user-id="<?php echo esc_attr( $pwexp_excluded_id ); ?>">
4436 <span class="user-display"><?php echo esc_html( $excluded_user->display_name . ' (' . $excluded_user->user_email . ')' ); ?></span>
4437 <button type="button" class="remove-user" aria-label="<?php esc_attr_e( 'Remove', 'vigilante' ); ?>">&times;</button>
4438 <input type="hidden" name="user_security[password_expiration][excluded_users][]" value="<?php echo esc_attr( $pwexp_excluded_id ); ?>">
4439 </div>
4440 <?php endforeach; ?>
4441 </div>
4442 </div>
4443 <p class="description"><?php esc_html_e( 'Listed users will be excluded from password expiration regardless of their role.', 'vigilante' ); ?></p>
4444 </td>
4445 </tr>
4446 </table>
4447 </div>
4448
4449 <!-- Email Verification -->
4450 <div id="vigilante-section-users-email-verify" class="vigilante-settings-section">
4451 <h2>
4452 <?php esc_html_e( 'Email verification', 'vigilante' ); ?>
4453 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4454 </h2>
4455 <p><?php esc_html_e( 'Require new users to verify their email address before logging in.', 'vigilante' ); ?></p>
4456
4457 <table class="form-table">
4458 <tr>
4459 <th scope="row"><?php esc_html_e( 'Enable Email Verification', 'vigilante' ); ?></th>
4460 <td>
4461 <label>
4462 <input type="checkbox" name="user_security[email_verification][enabled]" value="1" <?php checked( ! empty( $email_verify['enabled'] ) ); ?>>
4463 <?php esc_html_e( 'New users must verify their email before logging in', 'vigilante' ); ?>
4464 </label>
4465 </td>
4466 </tr>
4467 <tr>
4468 <th scope="row"><?php esc_html_e( 'Link Expiration', 'vigilante' ); ?></th>
4469 <td>
4470 <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">
4471 <?php esc_html_e( 'hours', 'vigilante' ); ?>
4472 </td>
4473 </tr>
4474 <tr>
4475 <th scope="row"><?php esc_html_e( 'Allow Resend', 'vigilante' ); ?></th>
4476 <td>
4477 <label>
4478 <input type="checkbox" name="user_security[email_verification][allow_resend]" value="1" <?php checked( ! empty( $email_verify['allow_resend'] ) ); ?>>
4479 <?php esc_html_e( 'Allow users to request a new verification email', 'vigilante' ); ?>
4480 </label>
4481 </td>
4482 </tr>
4483 <tr>
4484 <th scope="row"><?php esc_html_e( 'Auto-delete Unverified', 'vigilante' ); ?></th>
4485 <td>
4486 <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">
4487 <?php esc_html_e( 'days (0 = never)', 'vigilante' ); ?>
4488 <p class="description"><?php esc_html_e( 'Automatically delete users who never verify their email.', 'vigilante' ); ?></p>
4489 </td>
4490 </tr>
4491 </table>
4492 </div>
4493
4494 <p class="submit vigilante-submit-buttons">
4495 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
4496 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
4497 </button>
4498 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
4499 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
4500 </button>
4501 </p>
4502 </form>
4503
4504 <!-- ============================================================
4505 TOOLS SECTION - Actions and utilities (no save button)
4506 ============================================================ -->
4507 <div class="vigilante-tools-section">
4508 <h2 class="vigilante-tools-header">
4509 <?php esc_html_e( 'User security tools', 'vigilante' ); ?>
4510 </h2>
4511
4512 <!-- Force Password Reset -->
4513 <div class="vigilante-tool-box">
4514 <h3><?php esc_html_e( 'Force password reset', 'vigilante' ); ?></h3>
4515 <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>
4516
4517 <!-- Reset Specific Users -->
4518 <div class="vigilante-password-reset-box">
4519 <h4><?php esc_html_e( 'Reset specific users', 'vigilante' ); ?></h4>
4520
4521 <div class="vigilante-user-search-wrapper">
4522 <input type="text" id="vigilante-password-reset-search" class="regular-text" placeholder="<?php esc_attr_e( 'Search by username, email, or display name...', 'vigilante' ); ?>">
4523 <div id="vigilante-password-reset-results" class="vigilante-user-search-results" style="display: none;"></div>
4524 </div>
4525
4526 <div id="vigilante-password-reset-selected" class="vigilante-selected-users" style="display: none;">
4527 <strong><?php esc_html_e( 'Selected Users:', 'vigilante' ); ?></strong>
4528 <ul class="vigilante-selected-users-list"></ul>
4529 </div>
4530
4531 <p class="description" style="margin-top: 15px;">
4532 <span class="dashicons dashicons-email-alt" style="color: #2271b1;"></span>
4533 <?php esc_html_e( 'Selected users will receive an email with a password reset link.', 'vigilante' ); ?>
4534 </p>
4535
4536 <p class="submit">
4537 <button type="button" id="vigilante-reset-selected-users" class="button button-primary" disabled>
4538 <?php esc_html_e( 'Force Reset for Selected Users', 'vigilante' ); ?>
4539 </button>
4540 </p>
4541 </div>
4542
4543 <!-- Reset by Role -->
4544 <div class="vigilante-password-reset-box" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
4545 <h4><?php esc_html_e( 'Reset by role', 'vigilante' ); ?></h4>
4546 <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>
4547
4548 <?php
4549 $wp_roles = wp_roles();
4550 $user_counts = count_users();
4551 $avail_roles = $user_counts['avail_roles'] ?? array();
4552 $current_user = wp_get_current_user();
4553 $current_roles = $current_user->roles;
4554 ?>
4555
4556 <fieldset class="vigilante-role-checkboxes" style="margin-top: 10px;">
4557 <?php foreach ( $wp_roles->roles as $role_slug => $role_data ) :
4558 $count = $avail_roles[ $role_slug ] ?? 0;
4559 if ( 0 === $count ) {
4560 continue;
4561 }
4562 $role_name = translate_user_role( $role_data['name'] );
4563 ?>
4564 <label style="display: block; margin-bottom: 6px;">
4565 <input type="checkbox"
4566 class="vigilante-reset-role-checkbox"
4567 value="<?php echo esc_attr( $role_slug ); ?>"
4568 data-count="<?php echo absint( $count ); ?>">
4569 <?php
4570 printf(
4571 /* translators: 1: Role name, 2: Number of users */
4572 '%1$s <span class="description">(%2$d)</span>',
4573 esc_html( $role_name ),
4574 absint( $count )
4575 );
4576 ?>
4577 <?php if ( in_array( $role_slug, $current_roles, true ) ) : ?>
4578 <em class="description"><?php esc_html_e( '(includes you)', 'vigilante' ); ?></em>
4579 <?php endif; ?>
4580 </label>
4581 <?php endforeach; ?>
4582 </fieldset>
4583
4584 <div id="vigilante-reset-role-summary" style="display: none; margin-top: 10px;">
4585 <p>
4586 <span class="dashicons dashicons-groups" style="color: #2271b1;"></span>
4587 <strong id="vigilante-reset-role-count">0</strong>
4588 <?php esc_html_e( 'user(s) will be affected.', 'vigilante' ); ?>
4589 </p>
4590 </div>
4591
4592 <div class="vigilante-password-reset-options" id="vigilante-reset-role-self-option" style="display: none; margin-top: 10px;">
4593 <label>
4594 <input type="checkbox" id="vigilante-reset-role-include-self" value="1">
4595 <?php esc_html_e( 'Include myself (your current session will end)', 'vigilante' ); ?>
4596 </label>
4597 </div>
4598
4599 <p class="submit">
4600 <button type="button" id="vigilante-reset-by-role" class="button button-primary" disabled>
4601 <?php esc_html_e( 'Force Reset for Selected Roles', 'vigilante' ); ?>
4602 </button>
4603 </p>
4604 </div>
4605
4606 <!-- Reset All Users -->
4607 <div class="vigilante-password-reset-box" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
4608 <h4><?php esc_html_e( 'Reset all users', 'vigilante' ); ?></h4>
4609
4610 <?php
4611 $total_users = count_users();
4612 $total_count = $total_users['total_users'];
4613 ?>
4614 <p>
4615 <?php
4616 printf(
4617 /* translators: %d: Number of users */
4618 esc_html__( 'This will affect %d user(s).', 'vigilante' ),
4619 absint( $total_count )
4620 );
4621 ?>
4622 </p>
4623
4624 <p class="description" style="color: #d63638;">
4625 <span class="dashicons dashicons-warning"></span>
4626 <?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' ); ?>
4627 </p>
4628
4629 <div class="vigilante-password-reset-options" style="margin-top: 10px;">
4630 <label>
4631 <input type="checkbox" id="vigilante-reset-all-include-self" value="1">
4632 <?php esc_html_e( 'Include myself (your current session will end)', 'vigilante' ); ?>
4633 </label>
4634 </div>
4635
4636 <p class="submit">
4637 <button type="button" id="vigilante-reset-all-users" class="button" style="color: #d63638; border-color: #d63638;">
4638 <?php esc_html_e( 'Force Reset for ALL Users', 'vigilante' ); ?>
4639 </button>
4640 </p>
4641 </div>
4642 </div>
4643
4644 <!-- Pending Registrations -->
4645 <?php
4646 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
4647 $pending_users = $user_security->get_pending_users();
4648 ?>
4649 <div class="vigilante-tool-box vigilante-pending-users-section">
4650 <h3>
4651 <?php esc_html_e( 'Pending registrations', 'vigilante' ); ?>
4652 <?php if ( count( $pending_users ) > 0 ) : ?>
4653 <span class="vigilante-badge vigilante-badge-warning"><?php echo esc_html( count( $pending_users ) ); ?></span>
4654 <?php endif; ?>
4655 </h3>
4656
4657 <?php if ( empty( $registration['enabled'] ) ) : ?>
4658 <p class="description">
4659 <span class="dashicons dashicons-info" style="color: #72aee6;"></span>
4660 <?php esc_html_e( 'Registration approval is disabled. Enable it in the settings above to require manual approval for new users.', 'vigilante' ); ?>
4661 </p>
4662 <?php elseif ( empty( $pending_users ) ) : ?>
4663 <div class="vigilante-no-lockouts">
4664 <span class="dashicons dashicons-yes-alt"></span>
4665 <p><?php esc_html_e( 'No pending registrations.', 'vigilante' ); ?></p>
4666 </div>
4667 <?php else : ?>
4668 <table class="wp-list-table widefat fixed striped vigilante-pending-users-table">
4669 <thead>
4670 <tr>
4671 <th><?php esc_html_e( 'User', 'vigilante' ); ?></th>
4672 <th><?php esc_html_e( 'Email', 'vigilante' ); ?></th>
4673 <th><?php esc_html_e( 'Registered', 'vigilante' ); ?></th>
4674 <th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
4675 </tr>
4676 </thead>
4677 <tbody>
4678 <?php foreach ( $pending_users as $pending_user ) :
4679 $pending_since = get_user_meta( $pending_user->ID, 'vigilante_pending_since', true );
4680 ?>
4681 <tr data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>">
4682 <td>
4683 <?php echo get_avatar( $pending_user->ID, 32 ); ?>
4684 <strong><?php echo esc_html( $pending_user->user_login ); ?></strong>
4685 </td>
4686 <td><?php echo esc_html( $pending_user->user_email ); ?></td>
4687 <td>
4688 <?php
4689 if ( $pending_since ) {
4690 /* translators: %s: Time ago */
4691 printf( esc_html__( '%s ago', 'vigilante' ), esc_html( human_time_diff( $pending_since ) ) );
4692 } else {
4693 echo esc_html( $pending_user->user_registered );
4694 }
4695 ?>
4696 </td>
4697 <td>
4698 <button type="button" class="button button-small vigilante-approve-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>">
4699 <?php esc_html_e( 'Approve', 'vigilante' ); ?>
4700 </button>
4701 <button type="button" class="button button-small vigilante-reject-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>" style="color: #d63638;">
4702 <?php esc_html_e( 'Reject', 'vigilante' ); ?>
4703 </button>
4704 </td>
4705 </tr>
4706 <?php endforeach; ?>
4707 </tbody>
4708 </table>
4709 <?php endif; ?>
4710 </div>
4711
4712 <!-- Active Sessions Management -->
4713 <div class="vigilante-tool-box vigilante-session-management-section">
4714 <h3><?php esc_html_e( 'Active sessions', 'vigilante' ); ?></h3>
4715 <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>
4716
4717 <!-- Current user sessions -->
4718 <h4><?php esc_html_e( 'Your sessions', 'vigilante' ); ?></h4>
4719 <?php
4720 $current_user_id = get_current_user_id();
4721 $my_sessions = $user_security->get_user_sessions( $current_user_id );
4722 $has_corrupted = $user_security->has_corrupted_sessions( $current_user_id );
4723 $raw_count = $user_security->get_raw_session_count( $current_user_id );
4724 ?>
4725
4726 <?php if ( $has_corrupted && $raw_count > 0 ) : ?>
4727 <div class="notice notice-warning inline" style="margin: 10px 0;">
4728 <p>
4729 <span class="dashicons dashicons-warning" style="color: #dba617;"></span>
4730 <?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' ); ?>
4731 </p>
4732 </div>
4733 <?php endif; ?>
4734
4735 <?php if ( empty( $my_sessions ) ) : ?>
4736 <p class="description"><?php esc_html_e( 'No active sessions found.', 'vigilante' ); ?></p>
4737 <?php if ( $has_corrupted ) : ?>
4738 <p style="margin-top: 10px;">
4739 <button type="button" class="button vigilante-revoke-other-sessions" data-user-id="<?php echo esc_attr( $current_user_id ); ?>">
4740 <?php esc_html_e( 'Clean Up Corrupted Sessions', 'vigilante' ); ?>
4741 </button>
4742 </p>
4743 <?php endif; ?>
4744 <?php else : ?>
4745 <div class="vigilante-paginated-section">
4746 <div class="vigilante-fi-pagination-wrap"></div>
4747 <table class="wp-list-table widefat fixed striped vigilante-sessions-table vigilante-fi-paginated">
4748 <thead>
4749 <tr>
4750 <th><?php esc_html_e( 'Browser', 'vigilante' ); ?></th>
4751 <th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th>
4752 <th><?php esc_html_e( 'Login Time', 'vigilante' ); ?></th>
4753 <th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
4754 </tr>
4755 </thead>
4756 <tbody>
4757 <?php foreach ( $my_sessions as $session ) : ?>
4758 <tr data-token="<?php echo esc_attr( $session['token_hash'] ); ?>">
4759 <td><?php echo esc_html( $session['browser'] ); ?></td>
4760 <td><code><?php echo esc_html( $session['ip'] ); ?></code></td>
4761 <td>
4762 <?php
4763 if ( $session['login'] ) {
4764 /* translators: %s: Time ago */
4765 printf( esc_html__( '%s ago', 'vigilante' ), esc_html( human_time_diff( $session['login'] ) ) );
4766 } else {
4767 esc_html_e( 'Unknown', 'vigilante' );
4768 }
4769 ?>
4770 </td>
4771 <td>
4772 <?php if ( ! $session['is_current'] ) : ?>
4773 <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'] ); ?>">
4774 <?php esc_html_e( 'Revoke', 'vigilante' ); ?>
4775 </button>
4776 <?php else : ?>
4777 <span class="description"><?php esc_html_e( 'Current session', 'vigilante' ); ?></span>
4778 <?php endif; ?>
4779 </td>
4780 </tr>
4781 <?php endforeach; ?>
4782 </tbody>
4783 </table>
4784 </div>
4785
4786 <?php if ( count( $my_sessions ) > 1 || $has_corrupted ) : ?>
4787 <p style="margin-top: 10px;">
4788 <button type="button" class="button vigilante-revoke-other-sessions" data-user-id="<?php echo esc_attr( $current_user_id ); ?>">
4789 <?php esc_html_e( 'Revoke All Other Sessions', 'vigilante' ); ?>
4790 </button>
4791 </p>
4792 <?php endif; ?>
4793 <?php endif; ?>
4794
4795 <!-- Search user sessions (admin only) -->
4796 <h4 style="margin-top: 30px;"><?php esc_html_e( 'Manage user sessions', 'vigilante' ); ?></h4>
4797 <p class="description"><?php esc_html_e( 'Search for a user to view and manage their sessions.', 'vigilante' ); ?></p>
4798
4799 <div class="vigilante-user-search-wrapper" style="margin-top: 10px;">
4800 <input type="text" id="vigilante-session-user-search" class="regular-text" placeholder="<?php esc_attr_e( 'Search by username or email...', 'vigilante' ); ?>">
4801 <div id="vigilante-session-search-results" class="vigilante-user-search-results" style="display: none;"></div>
4802 </div>
4803
4804 <div id="vigilante-user-sessions-container" style="display: none; margin-top: 20px;">
4805 <h4 id="vigilante-sessions-user-name"></h4>
4806 <table class="wp-list-table widefat fixed striped vigilante-sessions-table">
4807 <thead>
4808 <tr>
4809 <th><?php esc_html_e( 'Browser', 'vigilante' ); ?></th>
4810 <th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th>
4811 <th><?php esc_html_e( 'Login Time', 'vigilante' ); ?></th>
4812 <th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
4813 </tr>
4814 </thead>
4815 <tbody id="vigilante-user-sessions-list">
4816 </tbody>
4817 </table>
4818 <p style="margin-top: 10px;">
4819 <button type="button" class="button vigilante-revoke-all-user-sessions" style="color: #d63638;">
4820 <?php esc_html_e( 'Revoke All Sessions', 'vigilante' ); ?>
4821 </button>
4822 </p>
4823 </div>
4824 </div>
4825 </div>
4826 <?php
4827 }
4828
4829 /**
4830 * Render WordPress Hardening tab
4831 */
4832 private function render_tab_wp_hardening() {
4833 $is_disabled = $this->render_module_disabled_notice( 'wp_hardening' );
4834 $options = $this->settings->get_section( 'wp_hardening' );
4835 ?>
4836 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="wp_hardening" <?php echo $is_disabled ? 'inert' : ''; ?>>
4837 <!-- Database Hardening (outside form save flow - uses its own AJAX action) -->
4838 <?php $vg_shared_locked = $this->shared_files_locked(); ?>
4839 <?php $this->render_shared_files_notice(); ?>
4840 <div id="vigilante-section-hardening-database" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
4841 <h2>
4842 <?php esc_html_e( 'Database Hardening', 'vigilante' ); ?>
4843 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
4844 <span class="vigilante-method-badge config"><?php esc_html_e( 'WP-CONFIG', 'vigilante' ); ?></span>
4845 </h2>
4846 <p><?php esc_html_e( 'Change the database table prefix to prevent SQL injection attacks that target default WordPress tables.', 'vigilante' ); ?></p>
4847
4848 <?php
4849 $db_prefix = new Vigilante_Database_Prefix();
4850 $current_prefix = $db_prefix->get_current_prefix();
4851 $is_default = $db_prefix->is_default_prefix();
4852 ?>
4853
4854 <?php if ( is_multisite() && ! $vg_shared_locked ) : ?>
4855 <div class="notice notice-warning inline" style="margin:10px 0 16px;padding:8px 12px;">
4856 <p style="margin:0;"><?php esc_html_e( 'Network-wide operation: it renames the tables of every site in the network and rewrites the wp-config.php they all share. Back up the whole database first, not just the main site.', 'vigilante' ); ?></p>
4857 </div>
4858 <?php endif; ?>
4859
4860 <table class="form-table">
4861 <tr>
4862 <th scope="row"><?php esc_html_e( 'Current prefix', 'vigilante' ); ?></th>
4863 <td>
4864 <code class="vigilante-db-current-prefix"><?php echo esc_html( $current_prefix ); ?></code>
4865 <?php if ( $is_default ) : ?>
4866 <span class="vigilante-inline-warning">
4867 <span class="dashicons dashicons-warning"></span>
4868 <?php esc_html_e( 'Default prefix detected. Changing it adds a layer of protection against automated SQL injection attacks.', 'vigilante' ); ?>
4869 </span>
4870 <?php else : ?>
4871 <span class="vigilante-inline-ok">
4872 <span class="dashicons dashicons-yes-alt"></span>
4873 <?php esc_html_e( 'Custom prefix in use.', 'vigilante' ); ?>
4874 </span>
4875 <?php endif; ?>
4876 </td>
4877 </tr>
4878 <tr>
4879 <th scope="row"><?php esc_html_e( 'New prefix', 'vigilante' ); ?></th>
4880 <td>
4881 <div class="vigilante-db-prefix-row">
4882 <code class="vigilante-db-new-prefix" id="vigilante-new-prefix"><?php echo esc_html( $db_prefix->generate_prefix() ); ?></code>
4883 <button type="button" class="button button-small vigilante-db-regenerate-prefix" title="<?php esc_attr_e( 'Generate new prefix', 'vigilante' ); ?>">
4884 <span class="dashicons dashicons-update"></span>
4885 </button>
4886 </div>
4887 </td>
4888 </tr>
4889 <tr>
4890 <th scope="row"></th>
4891 <td>
4892 <div class="vigilante-db-prefix-confirm">
4893 <label>
4894 <input type="checkbox" id="vigilante-prefix-backup-confirm">
4895 <?php esc_html_e( 'I understand this operation is irreversible and I have a current database backup', 'vigilante' ); ?>
4896 </label>
4897 <p class="description">
4898 <?php
4899 printf(
4900 /* translators: %s: Link to tools tab */
4901 esc_html__( 'Need a backup? %s first.', 'vigilante' ),
4902 '<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Download a database backup', 'vigilante' ) . '</a>'
4903 );
4904 ?>
4905 </p>
4906 </div>
4907 <button type="button" class="button button-primary vigilante-db-change-prefix" disabled data-original-text="<?php esc_attr_e( 'Change Database Prefix', 'vigilante' ); ?>">
4908 <?php esc_html_e( 'Change Database Prefix', 'vigilante' ); ?>
4909 </button>
4910 </td>
4911 </tr>
4912 </table>
4913 </div>
4914
4915 <!-- wp-config Security -->
4916 <?php
4917 $vg_shared_locked = $this->shared_files_locked();
4918 // Paint what is actually in force, not this site's unused copy.
4919 $vg_local_options = $options;
4920 $options = $this->get_section_for_display( 'wp_hardening' );
4921 ?>
4922 <?php $this->render_shared_files_notice(); ?>
4923 <div id="vigilante-section-hardening-wpconfig" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
4924 <h2>
4925 <?php esc_html_e( 'wp-config.php Security', 'vigilante' ); ?>
4926 <span class="vigilante-method-badge config"><?php esc_html_e( 'WP-CONFIG', 'vigilante' ); ?></span>
4927 </h2>
4928 <p><?php esc_html_e( 'Security constants added directly to wp-config.php file.', 'vigilante' ); ?></p>
4929
4930 <table class="form-table">
4931 <tr>
4932 <th scope="row"><?php esc_html_e( 'Disable File Editor', 'vigilante' ); ?></th>
4933 <td>
4934 <label>
4935 <input type="checkbox" name="wp_hardening[disallow_file_edit]" value="1" <?php checked( ! empty( $options['disallow_file_edit'] ) ); ?>>
4936 <?php esc_html_e( 'Disable plugin and theme editor in admin (DISALLOW_FILE_EDIT)', 'vigilante' ); ?>
4937 </label>
4938 </td>
4939 </tr>
4940 <tr>
4941 <th scope="row"><?php esc_html_e( 'Disable File Modifications', 'vigilante' ); ?></th>
4942 <td>
4943 <label>
4944 <input type="checkbox" name="wp_hardening[disallow_file_mods]" value="1" <?php checked( ! empty( $options['disallow_file_mods'] ) ); ?>>
4945 <?php esc_html_e( 'Disable all file modifications including updates (DISALLOW_FILE_MODS)', 'vigilante' ); ?>
4946 </label>
4947 <p class="description"><?php esc_html_e( '&#9888; Warning: This prevents automatic updates.', 'vigilante' ); ?></p>
4948 </td>
4949 </tr>
4950 <tr id="field-force-ssl-admin">
4951 <th scope="row"><?php esc_html_e( 'Force SSL Admin', 'vigilante' ); ?></th>
4952 <td>
4953 <label>
4954 <input type="checkbox" name="wp_hardening[force_ssl_admin]" value="1" <?php checked( ! empty( $options['force_ssl_admin'] ) ); ?>>
4955 <?php esc_html_e( 'Force HTTPS for admin area (FORCE_SSL_ADMIN)', 'vigilante' ); ?>
4956 </label>
4957 <p class="description"><?php esc_html_e( '&#9888; Warning: Only enable if your site fully supports HTTPS.', 'vigilante' ); ?></p>
4958 </td>
4959 </tr>
4960 <tr id="field-wp-debug">
4961 <th scope="row"><?php esc_html_e( 'Hide PHP errors from visitors', 'vigilante' ); ?></th>
4962 <td>
4963 <label>
4964 <input type="checkbox" name="wp_hardening[wp_debug]" value="1" <?php checked( ! empty( $options['wp_debug'] ) ); ?>>
4965 <?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' ); ?>
4966 </label>
4967 </td>
4968 </tr>
4969 <tr id="field-disable-wp-cron">
4970 <th scope="row"><?php esc_html_e( 'Disable WP Cron', 'vigilante' ); ?></th>
4971 <td>
4972 <label>
4973 <input type="checkbox" name="wp_hardening[disable_wp_cron]" value="1" <?php checked( ! empty( $options['disable_wp_cron'] ) ); ?>>
4974 <?php esc_html_e( 'Disable WordPress\'s page-view cron trigger (DISABLE_WP_CRON)', 'vigilante' ); ?>
4975 </label>
4976 <p class="description"><?php
4977 printf(
4978 /* translators: 1: opening <strong>, 2: closing </strong>, 3: opening <code>, 4: closing </code> */
4979 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' ),
4980 '<strong>',
4981 '</strong>',
4982 '<code>',
4983 '</code>'
4984 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded.
4985 ?></p>
4986 </td>
4987 </tr>
4988 </table>
4989 </div>
4990 <?php $options = $vg_local_options; ?>
4991
4992 <!-- Comment Security -->
4993 <div id="vigilante-section-hardening-xmlrpc" class="vigilante-settings-section">
4994 <h2>
4995 <?php esc_html_e( 'XML-RPC', 'vigilante' ); ?>
4996 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4997 </h2>
4998 <p><?php esc_html_e( 'The legacy remote interface. It is what the WordPress mobile app, Jetpack and remote managers talk to, and also the way pingback amplification and password guessing reach a site.', 'vigilante' ); ?></p>
4999
5000 <table class="form-table">
5001 <tr id="field-disable-xmlrpc">
5002 <th scope="row"><?php esc_html_e( 'XML-RPC access', 'vigilante' ); ?></th>
5003 <td>
5004 <?php $vig_xmlrpc_mode = Vigilante_Comment_Security::resolve_xmlrpc_mode( $this->settings ); ?>
5005 <select name="wp_hardening[xmlrpc_mode]">
5006 <option value="none" <?php selected( $vig_xmlrpc_mode, 'none' ); ?>>
5007 <?php esc_html_e( 'Leave XML-RPC enabled', 'vigilante' ); ?>
5008 </option>
5009 <option value="pingback" <?php selected( $vig_xmlrpc_mode, 'pingback' ); ?>>
5010 <?php esc_html_e( 'Block the pingback methods only', 'vigilante' ); ?>
5011 </option>
5012 <option value="full" <?php selected( $vig_xmlrpc_mode, 'full' ); ?>>
5013 <?php esc_html_e( 'Disable XML-RPC completely (recommended)', 'vigilante' ); ?>
5014 </option>
5015 </select>
5016 <p class="description"><?php esc_html_e( 'Disable it completely unless something still needs it, such as the WordPress mobile app, Jetpack or a remote manager; in that case block only the pingback methods, which closes the amplification vector and leaves the rest working. Pingbacks are also covered by the Comment Security setting just below, which additionally closes them for comments.', 'vigilante' ); ?></p>
5017 </td>
5018 </tr>
5019 </table>
5020 </div>
5021
5022 <div id="vigilante-section-hardening-comments" class="vigilante-settings-section">
5023 <h2>
5024 <?php esc_html_e( 'Comment Security', 'vigilante' ); ?>
5025 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5026 <span class="vigilante-method-badge settings"><?php esc_html_e( 'Settings', 'vigilante' ); ?></span>
5027 </h2>
5028 <p><?php esc_html_e( 'Comment protection using WordPress settings and PHP hooks.', 'vigilante' ); ?></p>
5029
5030 <table class="form-table">
5031 <tr>
5032 <th scope="row"><?php esc_html_e( 'Disable Pingbacks', 'vigilante' ); ?></th>
5033 <td>
5034 <label>
5035 <input type="checkbox" name="wp_hardening[disable_pingbacks]" value="1" <?php checked( ! empty( $options['disable_pingbacks'] ) ); ?>>
5036 <?php esc_html_e( 'Disable pingbacks (commonly exploited for DDoS)', 'vigilante' ); ?>
5037 </label>
5038 </td>
5039 </tr>
5040 <tr>
5041 <th scope="row"><?php esc_html_e( 'Disable Trackbacks', 'vigilante' ); ?></th>
5042 <td>
5043 <label>
5044 <input type="checkbox" name="wp_hardening[disable_trackbacks]" value="1" <?php checked( ! empty( $options['disable_trackbacks'] ) ); ?>>
5045 <?php esc_html_e( 'Disable trackbacks (rarely used legitimately)', 'vigilante' ); ?>
5046 </label>
5047 </td>
5048 </tr>
5049 <tr>
5050 <th scope="row"><?php esc_html_e( 'Require Moderation', 'vigilante' ); ?></th>
5051 <td>
5052 <label>
5053 <input type="checkbox" name="wp_hardening[require_comment_moderation]" value="1" <?php checked( ! empty( $options['require_comment_moderation'] ) ); ?>>
5054 <?php esc_html_e( 'All comments must be manually approved', 'vigilante' ); ?>
5055 </label>
5056 </td>
5057 </tr>
5058 <tr>
5059 <th scope="row"><?php esc_html_e( 'Close Old Comments', 'vigilante' ); ?></th>
5060 <td>
5061 <label>
5062 <input type="checkbox" name="wp_hardening[close_old_comments]" value="1" <?php checked( ! empty( $options['close_old_comments'] ) ); ?>>
5063 <?php esc_html_e( 'Automatically close comments on old posts after', 'vigilante' ); ?>
5064 </label>
5065 <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">
5066 <?php esc_html_e( 'days', 'vigilante' ); ?>
5067 </td>
5068 </tr>
5069 <tr>
5070 <th scope="row"><?php esc_html_e( 'Honeypot Protection', 'vigilante' ); ?></th>
5071 <td>
5072 <label>
5073 <input type="checkbox" name="wp_hardening[honeypot_comments]" value="1" <?php checked( ! empty( $options['honeypot_comments'] ) ); ?>>
5074 <?php esc_html_e( 'Add hidden honeypot field to catch bots', 'vigilante' ); ?>
5075 </label>
5076 </td>
5077 </tr>
5078 </table>
5079 </div>
5080
5081 <!-- Head Cleaner -->
5082 <div id="vigilante-section-hardening-headers" class="vigilante-settings-section">
5083 <h2>
5084 <?php esc_html_e( 'Header Cleanup', 'vigilante' ); ?>
5085 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5086 </h2>
5087 <p><?php esc_html_e( 'Remove meta tags from HTML head.', 'vigilante' ); ?></p>
5088
5089 <table class="form-table">
5090 <tr>
5091 <th scope="row"><?php esc_html_e( 'Remove Generator', 'vigilante' ); ?></th>
5092 <td>
5093 <label>
5094 <input type="checkbox" name="wp_hardening[remove_wp_generator]" value="1" <?php checked( ! empty( $options['remove_wp_generator'] ) ); ?>>
5095 <?php esc_html_e( 'Remove WordPress version from HTML head', 'vigilante' ); ?>
5096 </label>
5097 </td>
5098 </tr>
5099 <tr id="field-remove-wp-version-assets">
5100 <th scope="row"><?php esc_html_e( 'Remove version from assets', 'vigilante' ); ?></th>
5101 <td>
5102 <label>
5103 <input type="checkbox" name="wp_hardening[remove_wp_version_assets]" value="1" <?php checked( ! empty( $options['remove_wp_version_assets'] ) ); ?>>
5104 <?php esc_html_e( 'Remove WordPress version from script/style URLs (?ver=)', 'vigilante' ); ?>
5105 </label>
5106 <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>
5107 </td>
5108 </tr>
5109 <tr>
5110 <th scope="row"><?php esc_html_e( 'Remove RSD Link', 'vigilante' ); ?></th>
5111 <td>
5112 <label>
5113 <input type="checkbox" name="wp_hardening[remove_rsd_link]" value="1" <?php checked( ! empty( $options['remove_rsd_link'] ) ); ?>>
5114 <?php esc_html_e( 'Remove Really Simple Discovery link', 'vigilante' ); ?>
5115 </label>
5116 </td>
5117 </tr>
5118 <tr>
5119 <th scope="row"><?php esc_html_e( 'Remove WLW Manifest', 'vigilante' ); ?></th>
5120 <td>
5121 <label>
5122 <input type="checkbox" name="wp_hardening[remove_wlw_manifest]" value="1" <?php checked( ! empty( $options['remove_wlw_manifest'] ) ); ?>>
5123 <?php esc_html_e( 'Remove Windows Live Writer manifest link', 'vigilante' ); ?>
5124 </label>
5125 </td>
5126 </tr>
5127 <tr>
5128 <th scope="row"><?php esc_html_e( 'Remove Shortlink', 'vigilante' ); ?></th>
5129 <td>
5130 <label>
5131 <input type="checkbox" name="wp_hardening[remove_shortlink]" value="1" <?php checked( ! empty( $options['remove_shortlink'] ) ); ?>>
5132 <?php esc_html_e( 'Remove shortlink tag from header', 'vigilante' ); ?>
5133 </label>
5134 </td>
5135 </tr>
5136 <tr>
5137 <th scope="row"><?php esc_html_e( 'Remove REST API Link', 'vigilante' ); ?></th>
5138 <td>
5139 <label>
5140 <input type="checkbox" name="wp_hardening[remove_rest_api_link]" value="1" <?php checked( ! empty( $options['remove_rest_api_link'] ) ); ?>>
5141 <?php esc_html_e( 'Remove REST API discovery link from header', 'vigilante' ); ?>
5142 </label>
5143 <p class="description"><?php esc_html_e( '&#9888; Notice: Some plugins may need this link.', 'vigilante' ); ?></p>
5144 </td>
5145 </tr>
5146 </table>
5147 </div>
5148
5149 <!-- Feed Manager -->
5150 <div id="vigilante-section-hardening-rss" class="vigilante-settings-section">
5151 <h2>
5152 <?php esc_html_e( 'RSS Feed Settings', 'vigilante' ); ?>
5153 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5154 </h2>
5155 <p><?php esc_html_e( 'Control RSS/Atom feeds.', 'vigilante' ); ?></p>
5156
5157 <table class="form-table">
5158 <tr>
5159 <th scope="row"><?php esc_html_e( 'Disable Feeds', 'vigilante' ); ?></th>
5160 <td>
5161 <label>
5162 <input type="checkbox" name="wp_hardening[disable_feeds]" value="1" <?php checked( ! empty( $options['disable_feeds'] ) ); ?>>
5163 <?php esc_html_e( 'Completely disable RSS/Atom feeds', 'vigilante' ); ?>
5164 </label>
5165 </td>
5166 </tr>
5167 <tr>
5168 <th scope="row"><?php esc_html_e( 'Disable If No Content', 'vigilante' ); ?></th>
5169 <td>
5170 <label>
5171 <input type="checkbox" name="wp_hardening[disable_if_no_content]" value="1" <?php checked( ! empty( $options['disable_if_no_content'] ) ); ?>>
5172 <?php esc_html_e( 'Only disable feeds if site has no published posts', 'vigilante' ); ?>
5173 </label>
5174 </td>
5175 </tr>
5176 <tr>
5177 <th scope="row"><?php esc_html_e( 'Remove Feed Version', 'vigilante' ); ?></th>
5178 <td>
5179 <label>
5180 <input type="checkbox" name="wp_hardening[remove_feed_version]" value="1" <?php checked( ! empty( $options['remove_feed_version'] ) ); ?>>
5181 <?php esc_html_e( 'Remove WordPress version from feed generator tag', 'vigilante' ); ?>
5182 </label>
5183 </td>
5184 </tr>
5185 </table>
5186 </div>
5187
5188 <p class="submit vigilante-submit-buttons">
5189 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
5190 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
5191 </button>
5192 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
5193 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
5194 </button>
5195 </p>
5196 </form>
5197 <?php
5198 }
5199
5200 /**
5201 * Render activity log tab
5202 */
5203 private function render_tab_activity_log() {
5204 $is_disabled = $this->render_module_disabled_notice( 'activity_log' );
5205 $options = $this->settings->get_section( 'activity_log' );
5206 ?>
5207 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="activity_log" <?php echo $is_disabled ? 'inert' : ''; ?>>
5208 <div id="vigilante-section-audit-settings" class="vigilante-settings-section">
5209 <h2>
5210 <?php esc_html_e( 'Security Audit Settings', 'vigilante' ); ?>
5211 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5212 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
5213 </h2>
5214 <p><?php esc_html_e( 'Security event logging and auditing.', 'vigilante' ); ?></p>
5215
5216 <table class="form-table">
5217 <tr>
5218 <th scope="row"><?php esc_html_e( 'Retention', 'vigilante' ); ?></th>
5219 <td>
5220 <input type="number" name="activity_log[retention_days]" value="<?php echo esc_attr( $options['retention_days'] ?? 30 ); ?>" min="7" max="365" class="small-text">
5221 <?php esc_html_e( 'days', 'vigilante' ); ?>
5222 &nbsp;&nbsp;
5223 <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">
5224 <?php esc_html_e( 'max entries', 'vigilante' ); ?>
5225 <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>
5226 </td>
5227 </tr>
5228 <tr>
5229 <th scope="row"><?php esc_html_e( 'Events to Log', 'vigilante' ); ?></th>
5230 <td>
5231 <fieldset style="display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)); gap:6px 24px; max-width:600px;">
5232 <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>
5233 <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>
5234 <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>
5235 <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>
5236 <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>
5237 <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>
5238 <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>
5239 <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>
5240 <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>
5241 <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>
5242 </fieldset>
5243 <div class="notice notice-info inline" style="margin:10px 0 0;padding:8px 12px;">
5244 <p style="margin:0;">
5245 <?php esc_html_e( 'Firewall blocks, security events, and Vigilant settings changes are always logged regardless of the above selections.', 'vigilante' ); ?>
5246 </p>
5247 </div>
5248 </td>
5249 </tr>
5250 <tr>
5251 <th scope="row"><?php esc_html_e( 'Option Tracking', 'vigilante' ); ?></th>
5252 <td>
5253 <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>
5254 <br>
5255 <label><?php esc_html_e( 'Additional options to track:', 'vigilante' ); ?></label><br>
5256 <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>
5257 <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>
5258 </td>
5259 </tr>
5260 <tr>
5261 <th scope="row"><?php esc_html_e( 'Exclusions', 'vigilante' ); ?></th>
5262 <td>
5263 <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:16px; max-width:600px;">
5264 <div>
5265 <label><?php esc_html_e( 'Excluded user IDs:', 'vigilante' ); ?></label><br>
5266 <textarea name="activity_log[excluded_users]" rows="3" cols="25"><?php echo esc_textarea( implode( "\n", $options['excluded_users'] ?? array() ) ); ?></textarea>
5267 <p class="description"><?php esc_html_e( 'One user ID per line. Actions by these users will not be logged.', 'vigilante' ); ?></p>
5268 </div>
5269 <div>
5270 <label><?php esc_html_e( 'Excluded IPs:', 'vigilante' ); ?></label><br>
5271 <textarea name="activity_log[excluded_ips]" rows="3" cols="25"><?php echo esc_textarea( implode( "\n", $options['excluded_ips'] ?? array() ) ); ?></textarea>
5272 <p class="description"><?php esc_html_e( 'One IP per line. Requests from these IPs will not be logged.', 'vigilante' ); ?></p>
5273 </div>
5274 </div>
5275 </td>
5276 </tr>
5277 </table>
5278 </div>
5279
5280 <p class="submit vigilante-submit-buttons">
5281 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
5282 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
5283 </button>
5284 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
5285 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
5286 </button>
5287 </p>
5288 </form>
5289
5290 <?php
5291 // Audit Alerts — alerting layer on top of Security Audit. Its own
5292 // settings section and form, rendered right after the logging settings
5293 // so the tab reads as "log this, exclude that, and alert me about this".
5294 $alerts = $this->settings->get_section( 'audit_alerts' );
5295 $immediate = isset( $alerts['immediate'] ) ? $alerts['immediate'] : array();
5296 $threshold = isset( $alerts['threshold'] ) ? $alerts['threshold'] : array();
5297 $alert_severity = isset( $immediate['min_severity'] ) ? $immediate['min_severity'] : 'critical';
5298 $alert_window = isset( $threshold['window'] ) ? $threshold['window'] : '1h';
5299 $threshold_cats = isset( $threshold['categories'] ) ? (array) $threshold['categories'] : array();
5300 $cat_labels = Vigilante_Audit_Alerts::category_labels();
5301 ?>
5302 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="audit_alerts" <?php echo $is_disabled ? 'inert' : ''; ?>>
5303 <div id="vigilante-section-audit-alerts" class="vigilante-settings-section">
5304 <h2>
5305 <?php esc_html_e( 'Audit Alerts', 'vigilante' ); ?>
5306 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5307 </h2>
5308 <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>
5309
5310 <table class="form-table">
5311 <tr id="field-audit-alerts-immediate">
5312 <th scope="row"><?php esc_html_e( 'Immediate alerts', 'vigilante' ); ?></th>
5313 <td>
5314 <label>
5315 <input type="checkbox" name="audit_alerts[immediate][enabled]" value="1" <?php checked( ! empty( $immediate['enabled'] ) ); ?>>
5316 <?php esc_html_e( 'Email me as soon as a serious event is logged', 'vigilante' ); ?>
5317 </label>
5318 <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>
5319 </td>
5320 </tr>
5321 <tr>
5322 <th scope="row"><?php esc_html_e( 'Alert on severity', 'vigilante' ); ?></th>
5323 <td>
5324 <select name="audit_alerts[immediate][min_severity]">
5325 <option value="critical" <?php selected( $alert_severity, 'critical' ); ?>><?php esc_html_e( 'Critical only (recommended)', 'vigilante' ); ?></option>
5326 <option value="warning" <?php selected( $alert_severity, 'warning' ); ?>><?php esc_html_e( 'Warning and Critical', 'vigilante' ); ?></option>
5327 </select>
5328 <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>
5329 </td>
5330 </tr>
5331 <tr id="field-audit-alerts-threshold">
5332 <th scope="row"><?php esc_html_e( 'Threshold alerts', 'vigilante' ); ?></th>
5333 <td>
5334 <label>
5335 <input type="checkbox" name="audit_alerts[threshold][enabled]" value="1" <?php checked( ! empty( $threshold['enabled'] ) ); ?>>
5336 <?php esc_html_e( 'Email me when a category spikes within a time window', 'vigilante' ); ?>
5337 </label>
5338 <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>
5339 </td>
5340 </tr>
5341 <tr>
5342 <th scope="row"><?php esc_html_e( 'Time window', 'vigilante' ); ?></th>
5343 <td>
5344 <select name="audit_alerts[threshold][window]">
5345 <option value="30m" <?php selected( $alert_window, '30m' ); ?>><?php esc_html_e( '30 minutes', 'vigilante' ); ?></option>
5346 <option value="1h" <?php selected( $alert_window, '1h' ); ?>><?php esc_html_e( '1 hour', 'vigilante' ); ?></option>
5347 <option value="6h" <?php selected( $alert_window, '6h' ); ?>><?php esc_html_e( '6 hours', 'vigilante' ); ?></option>
5348 <option value="24h" <?php selected( $alert_window, '24h' ); ?>><?php esc_html_e( '24 hours', 'vigilante' ); ?></option>
5349 </select>
5350 <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>
5351 </td>
5352 </tr>
5353 <tr>
5354 <th scope="row"><?php esc_html_e( 'Thresholds per category', 'vigilante' ); ?></th>
5355 <td>
5356 <fieldset style="display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:8px 24px; max-width:760px;">
5357 <?php
5358 foreach ( $cat_labels as $cat_slug => $cat_label ) :
5359 $cat_value = isset( $threshold_cats[ $cat_slug ] ) ? (int) $threshold_cats[ $cat_slug ] : 0;
5360 ?>
5361 <label style="display:flex;align-items:center;gap:8px;justify-content:space-between;">
5362 <span><?php echo esc_html( $cat_label ); ?></span>
5363 <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">
5364 </label>
5365 <?php endforeach; ?>
5366 </fieldset>
5367 <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>
5368 </td>
5369 </tr>
5370 <tr>
5371 <th scope="row"><?php esc_html_e( "Don't repeat alerts", 'vigilante' ); ?></th>
5372 <td>
5373 <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">
5374 <?php esc_html_e( 'minutes', 'vigilante' ); ?>
5375 <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>
5376 </td>
5377 </tr>
5378
5379 <tr>
5380 <th scope="row"><?php esc_html_e( 'Recipients', 'vigilante' ); ?></th>
5381 <td>
5382 <p class="description" style="margin-top:0;">
5383 <?php
5384 printf(
5385 /* translators: %s: Link to notification settings */
5386 esc_html__( 'Alerts go to the recipients configured in %s.', 'vigilante' ),
5387 '<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>'
5388 );
5389 ?>
5390 </p>
5391 <p style="margin:8px 0 0;">
5392 <button type="button" class="button vigilante-test-email-btn" data-original-text="<?php esc_attr_e( 'Send test email', 'vigilante' ); ?>">
5393 <?php esc_html_e( 'Send test email', 'vigilante' ); ?>
5394 </button>
5395 <span class="vigilante-test-email-result" style="margin-left:8px;"></span>
5396 </p>
5397 <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>
5398 </td>
5399 </tr>
5400 </table>
5401 </div>
5402
5403 <p class="submit vigilante-submit-buttons">
5404 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
5405 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
5406 </button>
5407 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
5408 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
5409 </button>
5410 </p>
5411 </form>
5412
5413 <div id="vigilante-section-audit-recent" class="vigilante-settings-section">
5414 <h2><?php esc_html_e( 'Recent Activity', 'vigilante' ); ?></h2>
5415
5416 <?php
5417 $logs = $this->activity_log->get_logs( array( 'per_page' => 20 ) );
5418 $total_logs = $this->activity_log->get_logs_count();
5419
5420 // Label maps for translated display
5421 $type_labels = array(
5422 'login' => __( 'Login', 'vigilante' ),
5423 'user' => __( 'User', 'vigilante' ),
5424 'content' => __( 'Content', 'vigilante' ),
5425 'plugin' => __( 'Plugin', 'vigilante' ),
5426 'theme' => __( 'Theme', 'vigilante' ),
5427 'settings' => __( 'Settings', 'vigilante' ),
5428 'comment' => __( 'Comment', 'vigilante' ),
5429 'media' => __( 'Media', 'vigilante' ),
5430 'firewall' => __( 'Firewall', 'vigilante' ),
5431 'file' => __( 'File', 'vigilante' ),
5432 'security' => __( 'Security', 'vigilante' ),
5433 'system' => __( 'System', 'vigilante' ),
5434 );
5435 $severity_labels = array(
5436 'info' => __( 'Info', 'vigilante' ),
5437 'warning' => __( 'Warning', 'vigilante' ),
5438 'critical' => __( 'Critical', 'vigilante' ),
5439 );
5440
5441 $firewall_options = $this->settings->get_section( 'firewall' );
5442 $ip_whitelist = $firewall_options['ip_whitelist'] ?? array();
5443 $ip_blacklist = $firewall_options['ip_blacklist'] ?? array();
5444 $ua_whitelist = $firewall_options['ua_whitelist'] ?? array();
5445 $ua_blacklist = $firewall_options['ua_blacklist'] ?? array();
5446 ?>
5447
5448 <div class="vigilante-log-filters">
5449 <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">
5450 <select id="vigilante-log-type-filter">
5451 <option value=""><?php esc_html_e( 'All Types', 'vigilante' ); ?></option>
5452 <option value="login"><?php esc_html_e( 'Login', 'vigilante' ); ?></option>
5453 <option value="user"><?php esc_html_e( 'User', 'vigilante' ); ?></option>
5454 <option value="content"><?php esc_html_e( 'Content', 'vigilante' ); ?></option>
5455 <option value="plugin"><?php esc_html_e( 'Plugin', 'vigilante' ); ?></option>
5456 <option value="theme"><?php esc_html_e( 'Theme', 'vigilante' ); ?></option>
5457 <option value="settings"><?php esc_html_e( 'Settings', 'vigilante' ); ?></option>
5458 <option value="comment"><?php esc_html_e( 'Comment', 'vigilante' ); ?></option>
5459 <option value="media"><?php esc_html_e( 'Media', 'vigilante' ); ?></option>
5460 <option value="firewall"><?php esc_html_e( 'Firewall', 'vigilante' ); ?></option>
5461 <option value="file"><?php esc_html_e( 'File', 'vigilante' ); ?></option>
5462 <option value="security"><?php esc_html_e( 'Security', 'vigilante' ); ?></option>
5463 <option value="system"><?php esc_html_e( 'System', 'vigilante' ); ?></option>
5464 </select>
5465 <select id="vigilante-log-severity-filter">
5466 <option value=""><?php esc_html_e( 'All Severities', 'vigilante' ); ?></option>
5467 <option value="info"><?php esc_html_e( 'Info', 'vigilante' ); ?></option>
5468 <option value="warning"><?php esc_html_e( 'Warning', 'vigilante' ); ?></option>
5469 <option value="critical"><?php esc_html_e( 'Critical', 'vigilante' ); ?></option>
5470 </select>
5471 <select id="vigilante-log-method-filter">
5472 <option value=""><?php esc_html_e( 'All Methods', 'vigilante' ); ?></option>
5473 <option value="GET">GET</option>
5474 <option value="POST">POST</option>
5475 <option value="PUT">PUT</option>
5476 <option value="DELETE">DELETE</option>
5477 <option value="PATCH">PATCH</option>
5478 <option value="OPTIONS">OPTIONS</option>
5479 <option value="HEAD">HEAD</option>
5480 </select>
5481 <button type="button" id="vigilante-log-refresh" class="button"><?php esc_html_e( 'Refresh', 'vigilante' ); ?></button>
5482 <span class="vigilante-pagination" id="vigilante-log-pagination" data-total="<?php echo esc_attr( $total_logs ); ?>" data-per-page="20" data-page="1">
5483 <?php if ( $total_logs > 20 ) : ?>
5484 <button type="button" class="vigilante-page-first" title="<?php esc_attr_e( 'First page', 'vigilante' ); ?>" disabled>&laquo;</button>
5485 <button type="button" class="vigilante-page-prev" title="<?php esc_attr_e( 'Previous page', 'vigilante' ); ?>" disabled>&lsaquo;</button>
5486 <?php endif; ?>
5487 <span class="vigilante-page-info">
5488 <?php
5489 $showing = min( 20, $total_logs );
5490 printf(
5491 /* translators: 1: first item, 2: last item, 3: total items */
5492 esc_html__( '%1$d–%2$d of %3$d', 'vigilante' ),
5493 $total_logs > 0 ? 1 : 0,
5494 absint( $showing ),
5495 absint( $total_logs )
5496 );
5497 ?>
5498 </span>
5499 <?php if ( $total_logs > 20 ) : ?>
5500 <button type="button" class="vigilante-page-next" title="<?php esc_attr_e( 'Next page', 'vigilante' ); ?>">&rsaquo;</button>
5501 <button type="button" class="vigilante-page-last" title="<?php esc_attr_e( 'Last page', 'vigilante' ); ?>">&raquo;</button>
5502 <?php endif; ?>
5503 </span>
5504 </div>
5505
5506 <div class="vigilante-log-table-wrap">
5507 <table id="vigilante-activity-log-table" class="wp-list-table widefat striped">
5508 <thead>
5509 <tr>
5510 <th class="column-date"><?php esc_html_e( 'Date', 'vigilante' ); ?></th>
5511 <th class="column-type"><?php esc_html_e( 'Type', 'vigilante' ); ?></th>
5512 <th class="column-method"><?php esc_html_e( 'Method', 'vigilante' ); ?></th>
5513 <th class="column-severity"><?php esc_html_e( 'Severity', 'vigilante' ); ?></th>
5514 <th class="column-message"><?php esc_html_e( 'Message', 'vigilante' ); ?></th>
5515 <th class="column-user"><?php esc_html_e( 'User', 'vigilante' ); ?></th>
5516 <th class="column-ip"><?php esc_html_e( 'IP', 'vigilante' ); ?></th>
5517 <th class="column-details"><?php esc_html_e( 'Details', 'vigilante' ); ?></th>
5518 </tr>
5519 </thead>
5520 <tbody>
5521 <?php
5522 if ( empty( $logs ) ) :
5523 ?>
5524 <tr><td colspan="8"><?php esc_html_e( 'No log entries found.', 'vigilante' ); ?></td></tr>
5525 <?php else : ?>
5526 <?php foreach ( $logs as $log ) :
5527 $request_method = isset( $log->request_method ) ? $log->request_method : '';
5528 // Prepare details as a simple object
5529 $ip_val = (string) ( $log->ip_address ?? '' );
5530 $ua_val = (string) ( $log->user_agent ?? '' );
5531 $details = array(
5532 'id' => (int) $log->id,
5533 'type' => (string) ( $log->event_type ?? '' ),
5534 'action' => (string) ( $log->event_action ?? '' ),
5535 'message' => (string) ( $log->event_message ?? '' ),
5536 'user' => (string) ( $log->user_login ?? '' ),
5537 'ip' => $ip_val,
5538 'user_agent' => $ua_val,
5539 'request_method' => (string) $request_method,
5540 'date' => (string) ( $log->created_at ?? '' ),
5541 'severity' => (string) ( $log->severity ?? 'info' ),
5542 'is_ip_whitelisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_whitelist, true ) ),
5543 'is_ip_blacklisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_blacklist, true ) ),
5544 'is_ua_whitelisted' => ( '' !== $ua_val && in_array( $ua_val, $ua_whitelist, true ) ),
5545 'is_ua_blacklisted' => ( '' !== $ua_val && in_array( $ua_val, $ua_blacklist, true ) ),
5546 );
5547 $display_type = isset( $type_labels[ $log->event_type ] ) ? $type_labels[ $log->event_type ] : $log->event_type;
5548 $display_severity = isset( $severity_labels[ $log->severity ] ) ? $severity_labels[ $log->severity ] : $log->severity;
5549 ?>
5550 <tr class="vigilante-severity-<?php echo esc_attr( $log->severity ); ?>">
5551 <td><?php echo esc_html( $log->created_at ); ?></td>
5552 <td><?php echo esc_html( $display_type ); ?></td>
5553 <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>
5554 <td><span class="vigilante-badge vigilante-badge-<?php echo esc_attr( $log->severity ); ?>"><?php echo esc_html( $display_severity ); ?></span></td>
5555 <td><?php echo esc_html( $log->event_message ); ?></td>
5556 <td><?php echo esc_html( $log->user_login ?? '-' ); ?></td>
5557 <td><code><?php echo esc_html( $log->ip_address ); ?></code></td>
5558 <td>
5559 <button type="button" class="button button-small vigilante-view-log-details"
5560 data-details='<?php echo esc_attr( wp_json_encode( $details, JSON_HEX_APOS | JSON_HEX_QUOT ) ); ?>'>
5561 <?php esc_html_e( 'View', 'vigilante' ); ?>
5562 </button>
5563 </td>
5564 </tr>
5565 <?php endforeach; ?>
5566 <?php endif; ?>
5567 </tbody>
5568 </table>
5569 </div>
5570
5571 <!-- Log Details Modal -->
5572 <div id="vigilante-log-details-modal" class="vigilante-modal" style="display: none;">
5573 <div class="vigilante-modal-content">
5574 <span class="vigilante-modal-close">&times;</span>
5575 <h3><?php esc_html_e( 'Log Entry Details', 'vigilante' ); ?></h3>
5576 <div id="vigilante-log-details-content"></div>
5577 </div>
5578 </div>
5579
5580 <p>
5581 <button type="button" class="button vigilante-export-logs"><?php esc_html_e( 'Export Audit Log', 'vigilante' ); ?></button>
5582 <button type="button" class="button vigilante-clear-logs" style="color: #a00;"><?php esc_html_e( 'Clear All Logs', 'vigilante' ); ?></button>
5583 </p>
5584 </div>
5585 <?php
5586 }
5587
5588 /**
5589 * Render File Integrity tab
5590 */
5591 private function render_tab_file_integrity() {
5592 $is_disabled = $this->render_module_disabled_notice( 'file_integrity' );
5593 $options = $this->settings->get_section( 'file_integrity' );
5594 $last_scan = get_option( 'vigilante_last_integrity_scan' );
5595 $last_results = get_option( 'vigilante_last_integrity_results' );
5596 $ignored_files = get_option( 'vigilante_ignored_files', array() );
5597
5598 // Backward compat: convert old notify_on_changes to notify_level
5599 $notify_level = $options['notify_level'] ?? '';
5600 if ( empty( $notify_level ) ) {
5601 $notify_level = ! empty( $options['notify_on_changes'] ) ? 'all' : 'disabled';
5602 }
5603
5604 // Closed + Removed plugins data. Surfaced inside Last Scan Results so the
5605 // user sees file findings and plugin closures together (same tier of risk,
5606 // same UI), and as the trigger to keep Last Scan Results open even when no
5607 // file scan has run yet (the daily cron may have populated this section).
5608 //
5609 // Gating by last_check_time > 0 is how "Clear Previous Results" visually
5610 // resets this block: the option vigilante_plugin_status_last_check is
5611 // deleted on Clear, but the state map and the ignored list survive so the
5612 // next scan reconstructs without degrading a 'removed' slug. While
5613 // last_check is 0, we treat the plugin_status data as if it didn't exist.
5614 if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) {
5615 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
5616 }
5617 $closed_checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
5618 $closed_last_check = $closed_checker->get_last_check_time();
5619 if ( $closed_last_check > 0 ) {
5620 $closed_plugins = $closed_checker->get_closed_plugins();
5621 $ignored_closed_plugins = $closed_checker->get_ignored_closed_plugins();
5622 } else {
5623 $closed_plugins = array();
5624 $ignored_closed_plugins = array();
5625 }
5626 $has_closed = ! empty( $closed_plugins );
5627 $datetime_format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
5628 ?>
5629 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="file_integrity" <?php echo $is_disabled ? 'inert' : ''; ?>>
5630 <div id="vigilante-section-fi-monitoring" class="vigilante-settings-section">
5631 <h2>
5632 <?php esc_html_e( 'File Integrity Monitoring', 'vigilante' ); ?>
5633 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5634 </h2>
5635 <p><?php esc_html_e( 'Detects file modifications using WordPress.org checksums.', 'vigilante' ); ?></p>
5636
5637 <table class="form-table">
5638 <tr>
5639 <th scope="row"><?php esc_html_e( 'Automatic Scans', 'vigilante' ); ?></th>
5640 <td>
5641 <label>
5642 <input type="checkbox" name="file_integrity[auto_scan]" value="1" <?php checked( ! empty( $options['auto_scan'] ) ); ?>>
5643 <?php esc_html_e( 'Enable scheduled file integrity scans', 'vigilante' ); ?>
5644 </label>
5645 </td>
5646 </tr>
5647 <tr>
5648 <th scope="row"><?php esc_html_e( 'Scan Frequency', 'vigilante' ); ?></th>
5649 <td>
5650 <select name="file_integrity[scan_frequency]">
5651 <option value="daily" <?php selected( $options['scan_frequency'] ?? 'daily', 'daily' ); ?>><?php esc_html_e( 'Daily', 'vigilante' ); ?></option>
5652 <option value="weekly" <?php selected( $options['scan_frequency'] ?? 'daily', 'weekly' ); ?>><?php esc_html_e( 'Weekly', 'vigilante' ); ?></option>
5653 </select>
5654 </td>
5655 </tr>
5656 <tr>
5657 <th scope="row"><?php esc_html_e( 'Email Notifications', 'vigilante' ); ?></th>
5658 <td>
5659 <select name="file_integrity[notify_level]">
5660 <option value="all" <?php selected( $notify_level, 'all' ); ?>><?php esc_html_e( 'All issues (modified + suspicious)', 'vigilante' ); ?></option>
5661 <option value="suspicious_only" <?php selected( $notify_level, 'suspicious_only' ); ?>><?php esc_html_e( 'Suspicious files only', 'vigilante' ); ?></option>
5662 <option value="disabled" <?php selected( $notify_level, 'disabled' ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option>
5663 </select>
5664 <p class="description"><?php esc_html_e( '"Suspicious files only" reduces noise by skipping modified file notifications. Recommended for most sites.', 'vigilante' ); ?></p>
5665 </td>
5666 </tr>
5667 <tr>
5668 <th scope="row"><?php esc_html_e( 'Instant Alert', 'vigilante' ); ?></th>
5669 <td>
5670 <label>
5671 <input type="checkbox" name="file_integrity[instant_alert]" value="1" <?php checked( ! empty( $options['instant_alert'] ) ); ?>>
5672 <?php esc_html_e( 'Send immediate alert when modified, suspicious or additional files are detected, or when a closed plugin is found', 'vigilante' ); ?>
5673 </label>
5674 <p class="description"><?php esc_html_e( 'Fires even if the Email Notifications setting above is set to Disabled.', 'vigilante' ); ?></p>
5675 <p class="description">
5676 <?php
5677 printf(
5678 /* translators: %s: Link to notification settings */
5679 esc_html__( '&#9432; Notifications are sent to the recipients configured in %s.', 'vigilante' ),
5680 '<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>'
5681 );
5682 ?>
5683 </p>
5684 </td>
5685 </tr>
5686 <tr>
5687 <th scope="row"><?php esc_html_e( 'Test email', 'vigilante' ); ?></th>
5688 <td>
5689 <button type="button" class="button vigilante-test-email-btn" data-original-text="<?php esc_attr_e( 'Send test email', 'vigilante' ); ?>">
5690 <?php esc_html_e( 'Send test email', 'vigilante' ); ?>
5691 </button>
5692 <span class="vigilante-test-email-result" style="margin-left:8px;"></span>
5693 <p class="description"><?php esc_html_e( 'Sends a test message to the configured recipients to confirm email delivery works.', 'vigilante' ); ?></p>
5694 </td>
5695 </tr>
5696 <tr>
5697 <th scope="row"><?php esc_html_e( 'Scan Scope', 'vigilante' ); ?></th>
5698 <td>
5699 <fieldset>
5700 <label>
5701 <input type="checkbox" name="file_integrity[scan_core]" value="1" <?php checked( $options['scan_core'] ?? true ); ?>>
5702 <?php esc_html_e( 'Core files (compare against WordPress.org checksums)', 'vigilante' ); ?>
5703 </label>
5704 <br>
5705 <label>
5706 <input type="checkbox" name="file_integrity[scan_plugins]" value="1" <?php checked( $options['scan_plugins'] ?? true ); ?>>
5707 <?php esc_html_e( 'Plugins (WordPress.org repository plugins)', 'vigilante' ); ?>
5708 </label>
5709 <br>
5710 <label>
5711 <input type="checkbox" name="file_integrity[scan_themes]" value="1" <?php checked( $options['scan_themes'] ?? true ); ?>>
5712 <?php esc_html_e( 'Themes (WordPress.org repository themes)', 'vigilante' ); ?>
5713 </label>
5714 <br>
5715 <label>
5716 <input type="checkbox" name="file_integrity[scan_uploads]" value="1" <?php checked( $options['scan_uploads'] ?? true ); ?>>
5717 <?php esc_html_e( 'Uploads directory (detect PHP files, double extensions, .htaccess)', 'vigilante' ); ?>
5718 </label>
5719 <br>
5720 <label>
5721 <input type="checkbox" name="file_integrity[scan_critical_config]" value="1" <?php checked( $options['scan_critical_config'] ?? true ); ?>>
5722 <?php esc_html_e( 'Critical config files (wp-config.php, .htaccess baseline monitoring)', 'vigilante' ); ?>
5723 </label>
5724 <br>
5725 <label>
5726 <input type="checkbox" name="file_integrity[check_closed_plugins]" value="1" <?php checked( $options['check_closed_plugins'] ?? true ); ?>>
5727 <?php esc_html_e( 'Closed plugins (daily check against the WordPress.org repository)', 'vigilante' ); ?>
5728 </label>
5729 </fieldset>
5730 </td>
5731 </tr>
5732 <tr>
5733 <th scope="row"><?php esc_html_e( 'Excluded Paths', 'vigilante' ); ?></th>
5734 <td>
5735 <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>
5736 <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>
5737 </td>
5738 </tr>
5739 <tr>
5740 <th scope="row"><?php esc_html_e( 'Excluded Extensions', 'vigilante' ); ?></th>
5741 <td>
5742 <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>
5743 <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>
5744 </td>
5745 </tr>
5746 </table>
5747 </div>
5748
5749 <p class="submit vigilante-submit-buttons">
5750 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
5751 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
5752 </button>
5753 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
5754 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
5755 </button>
5756 <span class="vigilante-buttons-separator"></span>
5757 <button type="button" class="button button-primary vigilante-run-scan">
5758 <?php esc_html_e( 'Run Scan Now', 'vigilante' ); ?>
5759 </button>
5760 <button type="button" class="button vigilante-clear-scan-btn vigilante-clear-scan">
5761 <?php esc_html_e( 'Clear Previous Results', 'vigilante' ); ?>
5762 </button>
5763 </p>
5764 </form>
5765
5766 <div id="vigilante-scan-results" class="vigilante-settings-section" style="display:none;"></div>
5767
5768 <?php if ( $last_scan || $has_closed || $closed_last_check > 0 ) : ?>
5769 <div id="vigilante-section-fi-last-scan" class="vigilante-settings-section">
5770 <h2><?php esc_html_e( 'Last Scan Results', 'vigilante' ); ?></h2>
5771 <?php if ( $last_scan ) : ?>
5772 <p>
5773 <?php
5774 // Build the "X files scanned" hint inline with the date so it doesn't
5775 // need its own stat box (keeps the row compact when closed plugins are
5776 // present).
5777 $scanned_total = 0;
5778 if ( $last_results ) {
5779 $scanned_total = (int) ( $last_results['ok'] ?? 0 )
5780 + count( $last_results['modified'] ?? array() )
5781 + count( $last_results['suspicious'] ?? array() )
5782 + count( $last_results['extra'] ?? array() )
5783 + count( $ignored_files );
5784 }
5785 if ( $scanned_total > 0 ) {
5786 printf(
5787 /* translators: 1: date and time of last scan, 2: formatted file count */
5788 esc_html__( 'Last scan: %1$s (%2$s files scanned)', 'vigilante' ),
5789 esc_html( wp_date( $datetime_format, $last_scan ) ),
5790 esc_html( number_format_i18n( $scanned_total ) )
5791 );
5792 } else {
5793 printf(
5794 /* translators: %s: date and time of last scan */
5795 esc_html__( 'Last scan: %s', 'vigilante' ),
5796 esc_html( wp_date( $datetime_format, $last_scan ) )
5797 );
5798 }
5799 if ( $closed_last_check > 0 && $closed_last_check !== (int) $last_scan ) {
5800 echo ' &middot; ';
5801 printf(
5802 /* translators: %s: date and time of last closed plugins check */
5803 esc_html__( 'Closed plugins last checked: %s', 'vigilante' ),
5804 esc_html( wp_date( $datetime_format, $closed_last_check ) )
5805 );
5806 }
5807 ?>
5808 </p>
5809 <?php elseif ( $closed_last_check > 0 ) : ?>
5810 <p>
5811 <?php
5812 printf(
5813 /* translators: %s: date and time of last closed plugins check */
5814 esc_html__( 'Closed plugins last checked: %s &middot; the daily cron is running, no full integrity scan yet.', 'vigilante' ),
5815 esc_html( wp_date( $datetime_format, $closed_last_check ) )
5816 );
5817 ?>
5818 </p>
5819 <?php endif; ?>
5820 <div id="vigilante-last-scan-results">
5821 <?php if ( $last_results || $has_closed ) : ?>
5822 <div class="vigilante-scan-summary">
5823 <?php if ( $last_results ) : ?>
5824 <div class="vigilante-scan-stat vigilante-stat-ok">
5825 <span class="vigilante-stat-number"><?php echo esc_html( $last_results['ok'] ?? 0 ); ?></span>
5826 <span class="vigilante-stat-label"><?php esc_html_e( 'OK', 'vigilante' ); ?></span>
5827 </div>
5828 <div class="vigilante-scan-stat vigilante-stat-modified">
5829 <span class="vigilante-stat-number"><?php echo esc_html( count( $last_results['modified'] ?? array() ) ); ?></span>
5830 <span class="vigilante-stat-label"><?php esc_html_e( 'Modified', 'vigilante' ); ?></span>
5831 </div>
5832 <div class="vigilante-scan-stat vigilante-stat-suspicious">
5833 <span class="vigilante-stat-number"><?php echo esc_html( count( $last_results['suspicious'] ?? array() ) ); ?></span>
5834 <span class="vigilante-stat-label"><?php esc_html_e( 'Suspicious', 'vigilante' ); ?></span>
5835 </div>
5836 <div class="vigilante-scan-stat vigilante-stat-extra">
5837 <span class="vigilante-stat-number"><?php echo esc_html( count( $last_results['extra'] ?? array() ) ); ?></span>
5838 <span class="vigilante-stat-label"><?php esc_html_e( 'Extra', 'vigilante' ); ?></span>
5839 </div>
5840 <?php endif; ?>
5841 <?php if ( $has_closed ) : ?>
5842 <div class="vigilante-scan-stat vigilante-stat-suspicious">
5843 <span class="vigilante-stat-number" style="color: #d63638;"><?php echo (int) count( $closed_plugins ); ?></span>
5844 <span class="vigilante-stat-label"><?php esc_html_e( 'Closed/Removed', 'vigilante' ); ?></span>
5845 </div>
5846 <?php endif; ?>
5847 <?php if ( ! empty( $ignored_files ) ) : ?>
5848 <div class="vigilante-scan-stat vigilante-stat-ignored">
5849 <span class="vigilante-stat-number"><?php echo esc_html( count( $ignored_files ) ); ?></span>
5850 <span class="vigilante-stat-label"><?php esc_html_e( 'Ignored', 'vigilante' ); ?></span>
5851 </div>
5852 <?php endif; ?>
5853 </div>
5854
5855 <?php if ( ! empty( $last_results['suspicious'] ) ) : ?>
5856 <div class="vigilante-file-list vigilante-suspicious-files vigilante-paginated-section" data-bulk-mode="ignore">
5857 <h3 style="color: #d63638;"><?php esc_html_e( 'Suspicious Files', 'vigilante' ); ?></h3>
5858 <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>
5859 <div class="vigilante-fi-bulk-bar">
5860 <button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button>
5861 <span class="vigilante-fi-bulk-count" aria-live="polite"></span>
5862 </div>
5863 <div class="vigilante-fi-pagination-wrap"></div>
5864 <table class="wp-list-table widefat fixed striped vigilante-fi-paginated">
5865 <thead>
5866 <tr>
5867 <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>
5868 <th><?php esc_html_e( 'File', 'vigilante' ); ?></th>
5869 <th style="width: 250px;"><?php esc_html_e( 'Reason', 'vigilante' ); ?></th>
5870 <th style="width: 120px;"><?php esc_html_e( 'Type', 'vigilante' ); ?></th>
5871 <th style="width: 80px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
5872 </tr>
5873 </thead>
5874 <tbody>
5875 <?php
5876 foreach ( $last_results['suspicious'] as $item ) {
5877 $file_path = '';
5878 $file_reason = __( 'Unknown', 'vigilante' );
5879 $file_type = 'unknown';
5880
5881 if ( is_array( $item ) ) {
5882 if ( isset( $item['file'] ) ) {
5883 $file_path = $item['file'];
5884 }
5885 if ( isset( $item['reason'] ) ) {
5886 $file_reason = $item['reason'];
5887 }
5888 if ( isset( $item['type'] ) ) {
5889 $file_type = $item['type'];
5890 }
5891 } else {
5892 $file_path = (string) $item;
5893 }
5894 ?>
5895 <tr>
5896 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th>
5897 <td><code style="color: #d63638;"><?php echo esc_html( $file_path ); ?></code></td>
5898 <td><?php echo esc_html( $file_reason ); ?></td>
5899 <td><?php echo esc_html( $file_type ); ?></td>
5900 <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>
5901 </tr>
5902 <?php
5903 }
5904 ?>
5905 </tbody>
5906 </table>
5907 </div>
5908 <?php endif; ?>
5909
5910 <?php if ( ! empty( $last_results['extra'] ) ) : ?>
5911 <div class="vigilante-file-list vigilante-extra-files vigilante-paginated-section" data-bulk-mode="ignore">
5912 <h3 style="color: #b32d2e;"><?php esc_html_e( 'Extra Files', 'vigilante' ); ?></h3>
5913 <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>
5914 <div class="vigilante-fi-bulk-bar">
5915 <button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button>
5916 <span class="vigilante-fi-bulk-count" aria-live="polite"></span>
5917 </div>
5918 <div class="vigilante-fi-pagination-wrap"></div>
5919 <table class="wp-list-table widefat fixed striped vigilante-fi-paginated">
5920 <thead>
5921 <tr>
5922 <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>
5923 <th><?php esc_html_e( 'File', 'vigilante' ); ?></th>
5924 <th style="width: 250px;"><?php esc_html_e( 'Reason', 'vigilante' ); ?></th>
5925 <th style="width: 120px;"><?php esc_html_e( 'Type', 'vigilante' ); ?></th>
5926 <th style="width: 80px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
5927 </tr>
5928 </thead>
5929 <tbody>
5930 <?php
5931 foreach ( $last_results['extra'] as $item ) {
5932 $file_path = is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item;
5933 $file_reason = is_array( $item ) ? ( $item['reason'] ?? __( 'Unknown', 'vigilante' ) ) : __( 'Unknown', 'vigilante' );
5934 $file_type = is_array( $item ) ? ( $item['type'] ?? 'unknown' ) : 'unknown';
5935 ?>
5936 <tr>
5937 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th>
5938 <td><code style="color: #b32d2e;"><?php echo esc_html( $file_path ); ?></code></td>
5939 <td><?php echo esc_html( $file_reason ); ?></td>
5940 <td><?php echo esc_html( $file_type ); ?></td>
5941 <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>
5942 </tr>
5943 <?php
5944 }
5945 ?>
5946 </tbody>
5947 </table>
5948 </div>
5949 <?php endif; ?>
5950
5951 <?php
5952 // Split critical config files from regular modified files.
5953 // Computed unconditionally so the three sub-sections that consume
5954 // these arrays (Critical Config, Closed + Removed, Modified Files)
5955 // can render independently and in the order the team picked.
5956 $critical_modified = array();
5957 $regular_modified = array();
5958 if ( $last_results && ! empty( $last_results['modified'] ) ) {
5959 foreach ( $last_results['modified'] as $item ) {
5960 if ( is_array( $item ) && isset( $item['type'] ) && 'critical_config' === $item['type'] ) {
5961 $critical_modified[] = $item;
5962 } else {
5963 $regular_modified[] = $item;
5964 }
5965 }
5966 }
5967 ?>
5968
5969 <?php if ( ! empty( $critical_modified ) ) : ?>
5970 <div class="vigilante-file-list vigilante-critical-config-files">
5971 <h3 style="color: #e36210;"><?php esc_html_e( 'Critical config files modified', 'vigilante' ); ?></h3>
5972 <p class="description">
5973 <?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' ); ?>
5974 </p>
5975 <table class="wp-list-table widefat fixed striped">
5976 <thead>
5977 <tr>
5978 <th><?php esc_html_e( 'File', 'vigilante' ); ?></th>
5979 <th style="width: 200px;"><?php esc_html_e( 'Changes', 'vigilante' ); ?></th>
5980 <th style="width: 220px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
5981 </tr>
5982 </thead>
5983 <tbody>
5984 <?php foreach ( $critical_modified as $crit_item ) :
5985 $crit_file = $crit_item['file'] ?? '';
5986 $crit_baseline_size = $crit_item['baseline_size'] ?? 0;
5987 $crit_current_size = $crit_item['current_size'] ?? 0;
5988 $crit_diff = $crit_item['diff'] ?? array();
5989 $crit_id = sanitize_html_class( $crit_file );
5990 $added_count = is_array( $crit_diff ) ? count( $crit_diff['added'] ?? array() ) : 0;
5991 $removed_count = is_array( $crit_diff ) ? count( $crit_diff['removed'] ?? array() ) : 0;
5992 $diff_unavailable = is_array( $crit_diff ) && ! empty( $crit_diff['unavailable'] );
5993 ?>
5994 <tr>
5995 <td><code style="color: #e36210;"><?php echo esc_html( $crit_file ); ?></code></td>
5996 <td>
5997 <?php if ( ! $diff_unavailable ) : ?>
5998 <span style="color: #007017;">+<?php echo (int) $added_count; ?></span>
5999 <span style="color: #b32d2e;">-<?php echo (int) $removed_count; ?></span>
6000 <?php esc_html_e( 'lines', 'vigilante' ); ?><br>
6001 <?php endif; ?>
6002 <small style="color: #50575e;">
6003 <?php
6004 printf(
6005 /* translators: 1: baseline size, 2: current size */
6006 esc_html__( '%1$s &rarr; %2$s bytes', 'vigilante' ),
6007 esc_html( number_format_i18n( $crit_baseline_size ) ),
6008 esc_html( number_format_i18n( $crit_current_size ) )
6009 );
6010 ?>
6011 </small>
6012 </td>
6013 <td>
6014 <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' ); ?>">
6015 <?php esc_html_e( 'Review changes', 'vigilante' ); ?>
6016 </button>
6017 <button type="button" class="button button-small button-primary vigilante-approve-critical-file" data-file="<?php echo esc_attr( $crit_file ); ?>">
6018 <?php esc_html_e( 'Approve', 'vigilante' ); ?>
6019 </button>
6020 </td>
6021 </tr>
6022 <tr id="vigilante-critical-content-<?php echo esc_attr( $crit_id ); ?>" class="vigilante-critical-content-row" style="display:none;">
6023 <td colspan="3" style="padding: 0;">
6024 <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;">
6025 <?php if ( $diff_unavailable ) : ?>
6026 <p style="color: #50575e; font-style: italic; margin: 0;">
6027 <?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' ); ?>
6028 </p>
6029 <?php elseif ( empty( $crit_diff['added'] ) && empty( $crit_diff['removed'] ) ) : ?>
6030 <p style="color: #50575e; font-style: italic; margin: 0;">
6031 <?php esc_html_e( 'No line-level changes detected (may be whitespace or reordering).', 'vigilante' ); ?>
6032 </p>
6033 <?php else : ?>
6034 <?php if ( ! empty( $crit_diff['removed'] ) ) : ?>
6035 <?php foreach ( $crit_diff['removed'] as $rline ) : ?>
6036 <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>
6037 <?php endforeach; ?>
6038 <?php endif; ?>
6039 <?php if ( ! empty( $crit_diff['added'] ) ) : ?>
6040 <?php foreach ( $crit_diff['added'] as $aline ) : ?>
6041 <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>
6042 <?php endforeach; ?>
6043 <?php endif; ?>
6044 <?php endif; ?>
6045 </div>
6046 </td>
6047 </tr>
6048 <?php endforeach; ?>
6049 </tbody>
6050 </table>
6051 </div>
6052 <?php endif; ?>
6053
6054 <?php if ( $has_closed ) : ?>
6055 <div class="vigilante-file-list vigilante-closed-plugins">
6056 <h3 style="color: #d63638;"><?php esc_html_e( 'Closed + Removed Plugins', 'vigilante' ); ?></h3>
6057 <p class="description" style="color: #d63638;">
6058 <?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' ); ?>
6059 </p>
6060 <table class="wp-list-table widefat striped">
6061 <thead>
6062 <tr>
6063 <th><?php esc_html_e( 'Plugin', 'vigilante' ); ?></th>
6064 <th style="width: 70px;"><?php esc_html_e( 'Version', 'vigilante' ); ?></th>
6065 <th style="width: 90px;"><?php esc_html_e( 'State', 'vigilante' ); ?></th>
6066 <th style="width: 110px;"><?php esc_html_e( 'Closed date', 'vigilante' ); ?></th>
6067 <th><?php esc_html_e( 'Reason', 'vigilante' ); ?></th>
6068 <th style="width: 130px;"><?php esc_html_e( 'Detected', 'vigilante' ); ?></th>
6069 <th style="width: 90px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
6070 </tr>
6071 </thead>
6072 <tbody>
6073 <?php foreach ( $closed_plugins as $cp_slug => $cp_entry ) :
6074 $cp_state = $cp_entry['state'] ?? '';
6075 $cp_state_label = 'closed' === $cp_state ? __( 'Closed', 'vigilante' ) : __( 'Removed', 'vigilante' );
6076 $cp_state_color = 'closed' === $cp_state ? '#d63638' : '#b32d2e';
6077 $cp_reason = '';
6078 if ( ! empty( $cp_entry['closed_reason_text'] ) ) {
6079 $cp_reason = $cp_entry['closed_reason_text'];
6080 } elseif ( 'removed' === $cp_state ) {
6081 $cp_reason = __( 'Removed from repository (metadata hidden, typical of Security Issue closures)', 'vigilante' );
6082 }
6083 $cp_detected = isset( $cp_entry['first_detected'] ) ? (int) $cp_entry['first_detected'] : 0;
6084 ?>
6085 <tr>
6086 <td>
6087 <strong><?php echo esc_html( $cp_entry['name'] ?? $cp_slug ); ?></strong><br>
6088 <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>
6089 </td>
6090 <td><?php echo esc_html( $cp_entry['version'] ?? '' ); ?></td>
6091 <td><span style="color: <?php echo esc_attr( $cp_state_color ); ?>; font-weight: 600;"><?php echo esc_html( $cp_state_label ); ?></span></td>
6092 <td><?php echo esc_html( $cp_entry['closed_date'] ?? '' ); ?></td>
6093 <td><?php echo esc_html( $cp_reason ); ?></td>
6094 <td>
6095 <?php echo $cp_detected > 0 ? esc_html( wp_date( $datetime_format, $cp_detected ) ) : '&mdash;'; ?>
6096 </td>
6097 <td>
6098 <button type="button" class="button button-small vigilante-ignore-closed-plugin" data-slug="<?php echo esc_attr( $cp_slug ); ?>">
6099 <?php esc_html_e( 'Ignore', 'vigilante' ); ?>
6100 </button>
6101 </td>
6102 </tr>
6103 <?php endforeach; ?>
6104 </tbody>
6105 </table>
6106 </div>
6107 <?php endif; ?>
6108
6109 <?php if ( ! empty( $regular_modified ) ) : ?>
6110 <div class="vigilante-file-list vigilante-paginated-section" data-bulk-mode="ignore">
6111 <h3><?php esc_html_e( 'Modified Files', 'vigilante' ); ?></h3>
6112 <p class="description"><?php esc_html_e( 'These files (apparently) differ from the original WordPress or plugin versions.', 'vigilante' ); ?></p>
6113 <div class="vigilante-fi-bulk-bar">
6114 <button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button>
6115 <span class="vigilante-fi-bulk-count" aria-live="polite"></span>
6116 </div>
6117 <div class="vigilante-fi-pagination-wrap"></div>
6118 <table class="wp-list-table widefat fixed striped vigilante-fi-paginated">
6119 <thead>
6120 <tr>
6121 <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>
6122 <th><?php esc_html_e( 'File', 'vigilante' ); ?></th>
6123 <th style="width: 100px;"><?php esc_html_e( 'Type', 'vigilante' ); ?></th>
6124 <th style="width: 80px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
6125 </tr>
6126 </thead>
6127 <tbody>
6128 <?php
6129 foreach ( $regular_modified as $item ) {
6130 $file_path = '';
6131 $file_type = 'unknown';
6132
6133 if ( is_array( $item ) ) {
6134 if ( isset( $item['file'] ) ) {
6135 $file_path = $item['file'];
6136 }
6137 if ( isset( $item['type'] ) ) {
6138 $file_type = $item['type'];
6139 }
6140 } else {
6141 $file_path = (string) $item;
6142 }
6143 ?>
6144 <tr>
6145 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th>
6146 <td><code><?php echo esc_html( $file_path ); ?></code></td>
6147 <td><?php echo esc_html( $file_type ); ?></td>
6148 <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>
6149 </tr>
6150 <?php
6151 }
6152 ?>
6153 </tbody>
6154 </table>
6155 </div>
6156 <?php endif; ?>
6157
6158 <?php if ( $last_results && empty( $last_results['modified'] ) && empty( $last_results['suspicious'] ) && empty( $last_results['extra'] ) && ! $has_closed ) : ?>
6159 <p class="vigilante-all-clear" style="color: #00a32a; font-weight: bold;">
6160 <?php esc_html_e( 'Good Job! All files passed integrity check. No issues found.', 'vigilante' ); ?>
6161 </p>
6162 <?php endif; ?>
6163 <?php endif; ?>
6164 </div>
6165 </div>
6166 <?php endif; ?>
6167
6168 <?php if ( ! empty( $ignored_closed_plugins ) ) : ?>
6169 <div id="vigilante-section-fi-ignored-closed" class="vigilante-settings-section">
6170 <h2><?php esc_html_e( 'Ignored Closed + Removed Plugins', 'vigilante' ); ?></h2>
6171 <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>
6172 <table class="wp-list-table widefat fixed striped">
6173 <thead>
6174 <tr>
6175 <th><?php esc_html_e( 'Plugin', 'vigilante' ); ?></th>
6176 <th style="width: 110px;"><?php esc_html_e( 'State', 'vigilante' ); ?></th>
6177 <th style="width: 120px;"><?php esc_html_e( 'Closed date', 'vigilante' ); ?></th>
6178 <th style="width: 130px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
6179 </tr>
6180 </thead>
6181 <tbody>
6182 <?php foreach ( $ignored_closed_plugins as $icp_slug => $icp_entry ) :
6183 $icp_state = $icp_entry['state'] ?? '';
6184 $icp_state_label = 'closed' === $icp_state ? __( 'Closed', 'vigilante' ) : __( 'Removed', 'vigilante' );
6185 ?>
6186 <tr>
6187 <td>
6188 <strong><?php echo esc_html( $icp_entry['name'] ?? $icp_slug ); ?></strong><br>
6189 <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>
6190 </td>
6191 <td><?php echo esc_html( $icp_state_label ); ?></td>
6192 <td><?php echo esc_html( $icp_entry['closed_date'] ?? '' ); ?></td>
6193 <td>
6194 <button type="button" class="button button-small vigilante-unignore-closed-plugin" data-slug="<?php echo esc_attr( $icp_slug ); ?>">
6195 <?php esc_html_e( 'Stop ignoring', 'vigilante' ); ?>
6196 </button>
6197 </td>
6198 </tr>
6199 <?php endforeach; ?>
6200 </tbody>
6201 </table>
6202 <p style="margin-top: 10px;">
6203 <button type="button" class="button vigilante-clear-ignored-closed-plugins"><?php esc_html_e( 'Clear All Ignored Closed + Removed Plugins', 'vigilante' ); ?></button>
6204 </p>
6205 </div>
6206 <?php endif; ?>
6207
6208 <?php if ( ! empty( $ignored_files ) ) : ?>
6209 <div id="vigilante-section-fi-ignored" class="vigilante-settings-section">
6210 <h2><?php esc_html_e( 'Ignored Files', 'vigilante' ); ?></h2>
6211 <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>
6212 <div class="vigilante-paginated-section" data-bulk-mode="unignore">
6213 <div class="vigilante-fi-bulk-bar">
6214 <button type="button" class="button vigilante-bulk-unignore" disabled><?php esc_html_e( 'Stop ignoring selected', 'vigilante' ); ?></button>
6215 <span class="vigilante-fi-bulk-count" aria-live="polite"></span>
6216 </div>
6217 <div class="vigilante-fi-pagination-wrap"></div>
6218 <table class="wp-list-table widefat fixed striped vigilante-fi-paginated">
6219 <thead>
6220 <tr>
6221 <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>
6222 <th><?php esc_html_e( 'File', 'vigilante' ); ?></th>
6223 <th style="width: 120px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
6224 </tr>
6225 </thead>
6226 <tbody>
6227 <?php foreach ( $ignored_files as $file ) : ?>
6228 <tr>
6229 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file ); ?>"></th>
6230 <td><code><?php echo esc_html( $file ); ?></code></td>
6231 <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>
6232 </tr>
6233 <?php endforeach; ?>
6234 </tbody>
6235 </table>
6236 </div>
6237 <p style="margin-top: 10px;">
6238 <button type="button" class="button vigilante-clear-ignored"><?php esc_html_e( 'Clear All Ignored Files', 'vigilante' ); ?></button>
6239 </p>
6240 </div>
6241 <?php endif; ?>
6242 <?php
6243 }
6244
6245 /**
6246 * Render sidebar with promotional widgets
6247 */
6248 private function render_sidebar() {
6249 $promo_banner = new Vigilante_Promo_Banner( 'vigilante' );
6250 $promo_banner->render();
6251 }
6252
6253 /**
6254 * AJAX: Download a ZIP backup of the critical config files.
6255 *
6256 * Streams wp-config.php and .htaccess (and robots.txt if present) as a
6257 * downloadable archive. Config backups are no longer left as files under the
6258 * web root, so this hands the admin the archive directly.
6259 */
6260 public function ajax_download_files_backup() {
6261 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6262
6263 if ( ! current_user_can( 'manage_options' ) ) {
6264 wp_die( esc_html__( 'Permission denied.', 'vigilante' ), 403 );
6265 }
6266
6267 $backup_manager = new Vigilante_Backup_Manager();
6268 $result = $backup_manager->stream_files_zip();
6269
6270 // stream_files_zip() exits on success; only a WP_Error returns here.
6271 if ( is_wp_error( $result ) ) {
6272 wp_die( esc_html( $result->get_error_message() ), 500 );
6273 }
6274 }
6275
6276 /**
6277 * AJAX: Save settings
6278 */
6279 public function ajax_save_settings() {
6280 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6281
6282 if ( ! current_user_can( 'manage_options' ) ) {
6283 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6284 }
6285
6286 $section = isset( $_POST['section'] ) ? sanitize_key( $_POST['section'] ) : '';
6287
6288 // Handle $_POST['data'] based on type
6289 if ( isset( $_POST['data'] ) && is_array( $_POST['data'] ) ) {
6290 $data = map_deep( wp_unslash( $_POST['data'] ), 'sanitize_text_field' );
6291 } elseif ( isset( $_POST['data'] ) ) {
6292 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
6293 $raw_data = wp_unslash( $_POST['data'] );
6294 parse_str( $raw_data, $data );
6295 $data = map_deep( $data, 'sanitize_textarea_field' );
6296 } else {
6297 $data = array();
6298 }
6299
6300 if ( empty( $section ) ) {
6301 wp_send_json_error( __( 'Invalid section.', 'vigilante' ) );
6302 }
6303
6304 // Check if 2FA is being enabled or method changed (for notification sending)
6305 $send_2fa_notification = false;
6306 $send_login_url_notification = false;
6307
6308 if ( 'login_security' === $section ) {
6309 // Read old state from DB
6310 $db_options = get_option( Vigilante_Settings::OPTION_NAME, array() );
6311 $old_2fa_enabled = ! empty( $db_options['login_security']['two_factor']['enabled'] );
6312 $old_method = $db_options['login_security']['two_factor']['method'] ?? 'email';
6313
6314 // Check new state from submitted data
6315 $new_2fa_enabled = false;
6316 $notify_on_enable = false;
6317 $new_method = isset( $data['login_security']['two_factor']['method'] )
6318 ? sanitize_key( $data['login_security']['two_factor']['method'] )
6319 : 'email';
6320
6321 if ( isset( $data['login_security']['two_factor']['enabled'] ) ) {
6322 $new_2fa_enabled = filter_var( $data['login_security']['two_factor']['enabled'], FILTER_VALIDATE_BOOLEAN );
6323 }
6324 if ( isset( $data['login_security']['two_factor']['notify_on_enable'] ) ) {
6325 $notify_on_enable = filter_var( $data['login_security']['two_factor']['notify_on_enable'], FILTER_VALIDATE_BOOLEAN );
6326 }
6327
6328 // Send notification if:
6329 // 1. 2FA is being enabled (was off, now on) OR
6330 // 2. Method changed while 2FA is enabled
6331 if ( $notify_on_enable && $new_2fa_enabled ) {
6332 if ( ! $old_2fa_enabled || ( $old_2fa_enabled && $old_method !== $new_method ) ) {
6333 $send_2fa_notification = true;
6334 }
6335 }
6336
6337 // Check if login URL changed and notification is enabled
6338 $old_login_url = $db_options['login_security']['custom_login_url'] ?? '';
6339 $new_login_url = isset( $data['login_security']['custom_login_url'] )
6340 ? sanitize_title( $data['login_security']['custom_login_url'] )
6341 : '';
6342 $notify_on_url_change = isset( $data['login_security']['notify_on_login_url_change'] )
6343 ? filter_var( $data['login_security']['notify_on_login_url_change'], FILTER_VALIDATE_BOOLEAN )
6344 : false;
6345
6346 if ( $notify_on_url_change && ! empty( $new_login_url ) && $new_login_url !== $old_login_url ) {
6347 $send_login_url_notification = true;
6348 }
6349 }
6350
6351 // Get defaults
6352 $defaults = $this->settings->get_default_options();
6353
6354 // Read ONLY saved options from database (not merged with defaults)
6355 $saved_options = get_option( Vigilante_Settings::OPTION_NAME, array() );
6356
6357 // Handle modules
6358 if ( 'modules' === $section && isset( $data['modules'] ) ) {
6359 if ( ! isset( $saved_options['modules'] ) ) {
6360 $saved_options['modules'] = array();
6361 }
6362 foreach ( $data['modules'] as $module => $enabled ) {
6363 $module = sanitize_key( $module );
6364 $saved_options['modules'][ $module ] = in_array( $enabled, array( '1', 1, 'true', true ), true );
6365 }
6366 // Clear active preset when modules change
6367 update_option( 'vigilante_active_preset', '' );
6368 } else {
6369 // Process primary section
6370 if ( isset( $data[ $section ] ) && is_array( $data[ $section ] ) ) {
6371 $section_defaults = isset( $defaults[ $section ] ) ? $defaults[ $section ] : array();
6372 $current_section = isset( $saved_options[ $section ] ) ? $saved_options[ $section ] : array();
6373
6374 // Process the submitted data
6375 $processed = $this->process_section_data( $data[ $section ], $section_defaults, $current_section );
6376
6377 // Save the processed section
6378 $saved_options[ $section ] = $processed;
6379
6380 // Clear active preset when any section settings change
6381 update_option( 'vigilante_active_preset', '' );
6382 }
6383 }
6384
6385 // Clear cache before saving
6386 wp_cache_delete( Vigilante_Settings::OPTION_NAME, 'options' );
6387
6388 // Save to database
6389 update_option( Vigilante_Settings::OPTION_NAME, $saved_options );
6390
6391 // Clear the settings cache
6392 $this->settings->clear_cache();
6393
6394 // Apply changes based on section
6395 $this->apply_section_changes( $section, $saved_options );
6396
6397 // Send 2FA notifications after settings are saved
6398 $notification_result = null;
6399 if ( $send_2fa_notification ) {
6400 $notification_result = $this->send_2fa_enable_notifications();
6401 }
6402
6403 // Send login URL notifications after settings are saved
6404 $login_url_result = null;
6405 if ( $send_login_url_notification ) {
6406 $login_url_result = $this->send_login_url_notifications();
6407 }
6408
6409 // Build success message
6410 $message = __( 'Settings saved successfully.', 'vigilante' );
6411
6412 if ( $notification_result && $notification_result['sent'] > 0 ) {
6413 $message .= ' ' . sprintf(
6414 /* translators: %d: Number of emails sent */
6415 _n(
6416 '2FA notification sent to %d user.',
6417 '2FA notifications sent to %d users.',
6418 $notification_result['sent'],
6419 'vigilante'
6420 ),
6421 $notification_result['sent']
6422 );
6423 }
6424
6425 if ( $login_url_result && $login_url_result['sent'] > 0 ) {
6426 $message .= ' ' . sprintf(
6427 /* translators: %d: Number of emails sent */
6428 _n(
6429 'Login URL notification sent to %d user.',
6430 'Login URL notifications sent to %d users.',
6431 $login_url_result['sent'],
6432 'vigilante'
6433 ),
6434 $login_url_result['sent']
6435 );
6436 }
6437
6438 wp_send_json_success( $message );
6439 }
6440
6441 /**
6442 * Send 2FA enable notifications to users
6443 *
6444 * @return array Result with 'sent' and 'failed' counts.
6445 */
6446 private function send_2fa_enable_notifications() {
6447 $result = array(
6448 'sent' => 0,
6449 'failed' => 0,
6450 );
6451
6452 // Get settings for roles and method
6453 $login_security = $this->settings->get_section( 'login_security' );
6454 $two_factor = isset( $login_security['two_factor'] ) ? $login_security['two_factor'] : array();
6455 $roles = isset( $two_factor['enforced_roles'] ) ? $two_factor['enforced_roles'] : array( 'administrator' );
6456 $method = isset( $two_factor['method'] ) ? $two_factor['method'] : 'email';
6457
6458 if ( empty( $roles ) ) {
6459 $roles = array( 'administrator' );
6460 }
6461
6462 $excluded = isset( $two_factor['excluded_users'] ) ? array_map( 'absint', $two_factor['excluded_users'] ) : array();
6463
6464 // Get users with these roles
6465 $args = array(
6466 'role__in' => $roles,
6467 );
6468 if ( ! empty( $excluded ) ) {
6469 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Small excluded users list from settings.
6470 $args['exclude'] = $excluded;
6471 }
6472 $users = get_users( $args );
6473
6474 if ( empty( $users ) ) {
6475 return $result;
6476 }
6477
6478 $site_name = get_bloginfo( 'name' );
6479 $from_name = ! empty( $two_factor['email_from_name'] ) ? $two_factor['email_from_name'] : $site_name;
6480
6481 if ( 'totp' === $method ) {
6482 // Use TOTP class for styled HTML emails
6483 if ( ! class_exists( 'Vigilante_Two_Factor_TOTP' ) ) {
6484 require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-totp.php';
6485 }
6486 $totp = new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log );
6487
6488 foreach ( $users as $user ) {
6489 // Skip users who already have TOTP configured
6490 $totp_data = $this->database->get_totp_data( $user->ID );
6491 if ( $totp_data && ! empty( $totp_data['is_configured'] ) ) {
6492 continue;
6493 }
6494
6495 $sent = $totp->send_activation_email( $user, $site_name, $from_name );
6496
6497 if ( $sent ) {
6498 $this->database->mark_2fa_notified( $user->ID );
6499 $result['sent']++;
6500 } else {
6501 $result['failed']++;
6502 }
6503 }
6504 } else {
6505 // Email method - use existing email 2FA class
6506 if ( ! class_exists( 'Vigilante_Two_Factor_Email' ) ) {
6507 require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-email.php';
6508 }
6509 $email_2fa = new Vigilante_Two_Factor_Email( $this->settings, $this->database, $this->activity_log );
6510 return $email_2fa->send_activation_notifications( false );
6511 }
6512
6513 return $result;
6514 }
6515
6516 /**
6517 * Send login URL change notifications to users with admin access
6518 *
6519 * @return array Result with 'sent' and 'failed' counts.
6520 */
6521 private function send_login_url_notifications() {
6522 $login_options = $this->settings->get_section( 'login_security' );
6523 $custom_url = ! empty( $login_options['custom_login_url'] ) ? sanitize_title( $login_options['custom_login_url'] ) : '';
6524
6525 if ( empty( $custom_url ) ) {
6526 return array( 'sent' => 0, 'failed' => 0 );
6527 }
6528
6529 $login_url = home_url( $custom_url . '/' );
6530 $site_name = get_bloginfo( 'name' );
6531
6532 $admin_roles = array( 'administrator', 'editor', 'author', 'contributor' );
6533 $users = get_users( array( 'role__in' => $admin_roles ) );
6534
6535 if ( empty( $users ) ) {
6536 return array( 'sent' => 0, 'failed' => 0 );
6537 }
6538
6539 $subject = sprintf(
6540 /* translators: %s: Site name */
6541 __( '[%s] Your login URL has changed', 'vigilante' ),
6542 $site_name
6543 );
6544
6545 $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' ) );
6546 $body .= Vigilante_Email_Template::url_box( $login_url, __( 'Your new login URL:', 'vigilante' ) );
6547 $body .= Vigilante_Email_Template::alert_box( __( 'The old login address (wp-login.php) will no longer work.', 'vigilante' ) );
6548 $body .= Vigilante_Email_Template::button( $login_url, __( 'Go to login', 'vigilante' ) );
6549
6550 $sent = 0;
6551 $failed = 0;
6552
6553 foreach ( $users as $user ) {
6554 $result = Vigilante_Email_Template::send(
6555 $user->user_email,
6556 $subject,
6557 __( 'Login URL changed', 'vigilante' ),
6558 $body
6559 );
6560 if ( $result ) {
6561 $sent++;
6562 } else {
6563 $failed++;
6564 }
6565 }
6566
6567 if ( $this->activity_log ) {
6568 $this->activity_log->log(
6569 'login',
6570 'login_url_notified',
6571 sprintf(
6572 /* translators: 1: Sent count, 2: Failed count */
6573 __( 'Login URL notification sent on save: %1$d sent, %2$d failed', 'vigilante' ),
6574 $sent,
6575 $failed
6576 )
6577 );
6578 }
6579
6580 return array( 'sent' => $sent, 'failed' => $failed );
6581 }
6582
6583 /**
6584 * Process section data maintaining proper types from defaults
6585 *
6586 * @param array $submitted_data Data submitted from form.
6587 * @param array $defaults Default values for this section.
6588 * @param array $current Current saved values.
6589 * @param string $section_name Section name for special handling.
6590 * @return array Processed data.
6591 */
6592 private function process_section_data( $submitted_data, $defaults, $current, $section_name = '' ) {
6593 // Start with defaults, then merge current saved values
6594 $result = array_replace_recursive( $defaults, $current );
6595
6596 // Process each submitted value
6597 foreach ( $submitted_data as $key => $value ) {
6598 $key = sanitize_key( $key );
6599
6600 if ( is_array( $value ) ) {
6601 // Nested array (like rate_limiting)
6602 $nested_defaults = isset( $defaults[ $key ] ) && is_array( $defaults[ $key ] ) ? $defaults[ $key ] : array();
6603 $nested_current = isset( $result[ $key ] ) && is_array( $result[ $key ] ) ? $result[ $key ] : array();
6604 $result[ $key ] = $this->process_section_data( $value, $nested_defaults, $nested_current, $key );
6605 } else {
6606 // Determine type from default value
6607 $default_value = isset( $defaults[ $key ] ) ? $defaults[ $key ] : null;
6608
6609 if ( null === $default_value ) {
6610 // No default, check current value type or use as string
6611 $current_value = isset( $current[ $key ] ) ? $current[ $key ] : null;
6612 if ( is_bool( $current_value ) ) {
6613 $result[ $key ] = in_array( $value, array( '1', 1, 'true', true ), true );
6614 } elseif ( is_int( $current_value ) ) {
6615 $result[ $key ] = intval( $value );
6616 } elseif ( is_array( $current_value ) ) {
6617 $result[ $key ] = is_string( $value ) ? array_filter( array_map( 'trim', explode( "\n", $value ) ) ) : (array) $value;
6618 } else {
6619 $result[ $key ] = sanitize_text_field( $value );
6620 }
6621 } elseif ( is_bool( $default_value ) ) {
6622 // Boolean: '1', 1, 'true' become true; '0', 0, '', 'false' become false
6623 $result[ $key ] = in_array( $value, array( '1', 1, 'true', true ), true );
6624 } elseif ( is_int( $default_value ) ) {
6625 // Integer - with special handling for time fields shown in minutes
6626 $int_value = intval( $value );
6627
6628 // Convert minutes to seconds for login_security duration fields
6629 // These are displayed as minutes in the form but stored as seconds
6630 if ( in_array( $key, array( 'lockout_duration', 'max_lockout_duration' ), true ) ) {
6631 $int_value = $int_value * 60;
6632 }
6633
6634 $result[ $key ] = $int_value;
6635 } elseif ( is_array( $default_value ) ) {
6636 // Array from textarea (e.g., IP lists)
6637 if ( is_string( $value ) ) {
6638 $result[ $key ] = array_filter( array_map( 'trim', explode( "\n", $value ) ) );
6639 } else {
6640 $result[ $key ] = (array) $value;
6641 }
6642 } else {
6643 // String - preserve newlines for textarea fields
6644 if ( is_string( $value ) && ( strpos( $value, "\n" ) !== false || strpos( $value, "\r" ) !== false ) ) {
6645 $result[ $key ] = sanitize_textarea_field( $value );
6646 } else {
6647 $result[ $key ] = sanitize_text_field( $value );
6648 }
6649 }
6650 }
6651 }
6652
6653 // Handle unchecked checkboxes: HTML forms don't submit unchecked boxes
6654 // If a boolean field exists in defaults but NOT in submitted_data, set it to false
6655 //
6656 // EXCEPTION: the top-level 'enabled' flag of every section is the
6657 // module's master switch and is controlled by the Dashboard module
6658 // toggle, NOT by a checkbox inside the section's form. Treating it
6659 // like a regular checkbox here would silently switch the module off
6660 // every time the user saves the tab — see the REST API enabled=false
6661 // regression. We only skip it at the top level (when section_name is
6662 // empty); nested 'enabled' fields like security_headers.csp.enabled
6663 // are real checkboxes and must keep the auto-unset behaviour.
6664 $preserve_top_level = array( 'enabled' );
6665
6666 foreach ( $defaults as $key => $default_value ) {
6667 if ( '' === $section_name && in_array( $key, $preserve_top_level, true ) ) {
6668 continue;
6669 }
6670 if ( is_bool( $default_value ) && ! array_key_exists( $key, $submitted_data ) ) {
6671 $result[ $key ] = false;
6672 } elseif ( is_array( $default_value ) && ! isset( $submitted_data[ $key ] ) ) {
6673 // Check if this is a flat value list (like excluded_users, enforced_roles, ip_whitelist)
6674 // vs a nested settings group (like rate_limiting, two_factor)
6675 // Flat lists: default is empty array OR all values are scalar
6676 $is_value_list = empty( $default_value );
6677 if ( ! $is_value_list ) {
6678 $is_value_list = true;
6679 foreach ( $default_value as $dv ) {
6680 if ( ! is_scalar( $dv ) ) {
6681 $is_value_list = false;
6682 break;
6683 }
6684 }
6685 }
6686
6687 if ( $is_value_list ) {
6688 // All items removed - reset to empty array
6689 $result[ $key ] = array();
6690 } else {
6691 // Nested settings group - handle boolean children
6692 foreach ( $default_value as $nested_key => $nested_default ) {
6693 if ( is_bool( $nested_default ) && isset( $result[ $key ] ) && is_array( $result[ $key ] ) ) {
6694 $nested_submitted = isset( $submitted_data[ $key ] ) && is_array( $submitted_data[ $key ] )
6695 ? $submitted_data[ $key ]
6696 : array();
6697 if ( ! array_key_exists( $nested_key, $nested_submitted ) ) {
6698 $result[ $key ][ $nested_key ] = false;
6699 }
6700 }
6701 }
6702 }
6703 }
6704 }
6705
6706 return $result;
6707 }
6708
6709 /**
6710 * AJAX: Export settings
6711 */
6712 public function ajax_export_settings() {
6713 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6714
6715 if ( ! current_user_can( 'manage_options' ) ) {
6716 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6717 }
6718
6719 $options = $this->settings->get_all_options();
6720 $filename = 'vigilante-settings-' . gmdate( 'Y-m-d-His' ) . '.json';
6721
6722 wp_send_json_success( array(
6723 'content' => wp_json_encode( $options, JSON_PRETTY_PRINT ),
6724 'filename' => $filename,
6725 ) );
6726 }
6727
6728 /**
6729 * AJAX: Import settings
6730 */
6731 public function ajax_import_settings() {
6732 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6733
6734 if ( ! current_user_can( 'manage_options' ) ) {
6735 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6736 }
6737
6738 // Accept both 'settings' (from JS) and 'content' (legacy).
6739 // Do NOT run sanitize_text_field() on the raw payload: it calls
6740 // wp_strip_all_tags() internally, which removes any "<...>" substring
6741 // and turns a valid export JSON into garbage if any stored value
6742 // contains < or > (htaccess snippets, email templates, etc.). The
6743 // real sanitization happens after json_decode(), via map_deep() on
6744 // the parsed array.
6745 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
6746 $content = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : '';
6747 if ( empty( $content ) ) {
6748 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
6749 $content = isset( $_POST['content'] ) ? wp_unslash( $_POST['content'] ) : '';
6750 }
6751
6752 if ( empty( $content ) || ! is_string( $content ) ) {
6753 wp_send_json_error( __( 'No content provided.', 'vigilante' ) );
6754 }
6755
6756 $imported = json_decode( $content, true );
6757
6758 if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $imported ) ) {
6759 wp_send_json_error( __( 'Invalid JSON format.', 'vigilante' ) );
6760 }
6761
6762 // Sanitize imported data recursively
6763 $imported = map_deep( $imported, 'sanitize_text_field' );
6764
6765 // Validate structure
6766 $defaults = $this->settings->get_default_options();
6767 $merged = array_replace_recursive( $defaults, $imported );
6768
6769 // Save
6770 update_option( Vigilante_Settings::OPTION_NAME, $merged );
6771 $this->settings->clear_cache();
6772
6773 // Re-evaluate the active preset marker. The imported config may match
6774 // a known preset exactly, partially, or not at all — without this step
6775 // the dashboard would keep showing whatever preset was active before
6776 // the import even if the new config no longer matches it.
6777 $matched_preset = $this->detect_matching_preset( $merged );
6778 if ( null === $matched_preset ) {
6779 delete_option( 'vigilante_active_preset' );
6780 } else {
6781 update_option( 'vigilante_active_preset', $matched_preset );
6782 }
6783
6784 // Apply file changes after import
6785 $this->apply_all_file_changes( $merged );
6786
6787 // Refresh the Security Analyzer score so the dashboard widget reflects
6788 // the imported config rather than the pre-import scan. Reuse the
6789 // post-Under-Attack scan hook (same job: full scan, async).
6790 if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) {
6791 wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' );
6792 }
6793
6794 wp_send_json_success( __( 'Settings imported successfully.', 'vigilante' ) );
6795 }
6796
6797 /**
6798 * Detect whether a vigilante_options array matches a known preset.
6799 *
6800 * A preset matches when every field the preset explicitly declares is
6801 * present in the config with the same (normalised) value. Fields outside
6802 * the preset are ignored — they may have been modified by the user before
6803 * applying the preset and do not invalidate the match. This mirrors how
6804 * apply_preset() now layers presets on top of the user's existing config.
6805 *
6806 * @param array $options The current/imported vigilante_options.
6807 * @return string|null Preset id ('standard', 'maximum') or null if custom.
6808 */
6809 private function detect_matching_preset( $options ) {
6810 if ( ! is_array( $options ) ) {
6811 return null;
6812 }
6813
6814 $presets = $this->settings->get_presets();
6815
6816 foreach ( $presets as $preset_id => $preset_data ) {
6817 unset( $preset_data['name'], $preset_data['description'] );
6818 if ( $this->preset_subset_matches( $preset_data, $options ) ) {
6819 return $preset_id;
6820 }
6821 }
6822
6823 return null;
6824 }
6825
6826 /**
6827 * Check whether every leaf value inside $preset_subset exists with the
6828 * same (normalised) value at the same path inside $config.
6829 *
6830 * @param mixed $preset_subset Branch of the preset definition.
6831 * @param mixed $config Same branch in the live/imported config.
6832 * @return bool
6833 */
6834 private function preset_subset_matches( $preset_subset, $config ) {
6835 if ( is_array( $preset_subset ) ) {
6836 if ( ! is_array( $config ) ) {
6837 return false;
6838 }
6839 foreach ( $preset_subset as $key => $value ) {
6840 if ( ! array_key_exists( $key, $config ) ) {
6841 return false;
6842 }
6843 if ( ! $this->preset_subset_matches( $value, $config[ $key ] ) ) {
6844 return false;
6845 }
6846 }
6847 return true;
6848 }
6849
6850 return $this->normalise_scalar_for_compare( $preset_subset ) === $this->normalise_scalar_for_compare( $config );
6851 }
6852
6853 /**
6854 * Normalise a scalar value so that the variants WordPress and the form
6855 * layer routinely produce ('1' / 1 / true → "1"; '' / '0' / 0 / false /
6856 * null → "") compare equal. Other values become strings unchanged.
6857 *
6858 * @param mixed $value
6859 * @return string
6860 */
6861 private function normalise_scalar_for_compare( $value ) {
6862 if ( is_bool( $value ) ) {
6863 return $value ? '1' : '';
6864 }
6865 if ( null === $value ) {
6866 return '';
6867 }
6868 if ( is_int( $value ) || is_float( $value ) ) {
6869 return (string) $value;
6870 }
6871 if ( is_string( $value ) ) {
6872 if ( 'true' === $value ) {
6873 return '1';
6874 }
6875 if ( 'false' === $value ) {
6876 return '';
6877 }
6878 return $value;
6879 }
6880 // Arrays and objects shouldn't reach here (handled by recursion above),
6881 // but if they do, fall back to a stable comparable representation.
6882 return wp_json_encode( $value );
6883 }
6884
6885 /**
6886 * AJAX: Apply preset
6887 */
6888 public function ajax_apply_preset() {
6889 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6890
6891 if ( ! current_user_can( 'manage_options' ) ) {
6892 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6893 }
6894
6895 $preset = isset( $_POST['preset'] ) ? sanitize_key( $_POST['preset'] ) : '';
6896
6897 // Handle reset to defaults
6898 if ( 'reset' === $preset ) {
6899 $defaults = Vigilante_Settings::get_defaults_preserving_user_data( get_option( Vigilante_Settings::OPTION_NAME, array() ) );
6900 update_option( Vigilante_Settings::OPTION_NAME, $defaults );
6901 $this->settings->clear_cache();
6902
6903 // Clear active preset
6904 update_option( 'vigilante_active_preset', '' );
6905
6906 // Apply file changes after reset
6907 $this->apply_all_file_changes( $defaults );
6908
6909 wp_send_json_success( __( 'Settings reset to defaults.', 'vigilante' ) );
6910 return;
6911 }
6912
6913 $presets = $this->settings->get_presets();
6914
6915 if ( ! isset( $presets[ $preset ] ) ) {
6916 wp_send_json_error( __( 'Invalid preset.', 'vigilante' ) );
6917 }
6918
6919 $preset_options = $presets[ $preset ];
6920 unset( $preset_options['name'], $preset_options['description'] );
6921
6922 // Layer the preset on top of the user's CURRENT configuration, not on
6923 // top of defaults. This way applying a preset only changes the fields
6924 // the preset explicitly mentions; everything else stays as the user
6925 // had it. For example, applying Maximum will not flip HSTS off if the
6926 // user had it on — Maximum doesn't touch HSTS, so it's left alone.
6927 // Use "Reset to Defaults" if a clean slate is needed.
6928 $current = get_option( Vigilante_Settings::OPTION_NAME, array() );
6929 if ( ! is_array( $current ) ) {
6930 $current = array();
6931 }
6932 // Make sure all known keys exist before merging — the merge does not
6933 // invent keys that are missing on both sides.
6934 $current = Vigilante_Settings::merge_preset( $this->settings->get_default_options(), $current );
6935
6936 $merged = Vigilante_Settings::merge_preset( $current, $preset_options );
6937
6938 update_option( Vigilante_Settings::OPTION_NAME, $merged );
6939 $this->settings->clear_cache();
6940
6941 // Save active preset
6942 update_option( 'vigilante_active_preset', $preset );
6943
6944 // Apply file changes after preset
6945 $this->apply_all_file_changes( $merged );
6946
6947 wp_send_json_success( __( 'Preset applied successfully.', 'vigilante' ) );
6948 }
6949
6950 /**
6951 * AJAX: Reset a specific section to defaults
6952 */
6953 public function ajax_reset_section() {
6954 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6955
6956 if ( ! current_user_can( 'manage_options' ) ) {
6957 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6958 }
6959
6960 $section = isset( $_POST['section'] ) ? sanitize_key( $_POST['section'] ) : '';
6961
6962 if ( empty( $section ) ) {
6963 wp_send_json_error( __( 'No section specified.', 'vigilante' ) );
6964 }
6965
6966 // Get current options and defaults. get_defaults_preserving_user_data()
6967 // applies the tweaks a fresh installation gets, so the button and a new
6968 // install agree, and keeps whatever the owner typed in.
6969 $current_options = $this->settings->get_all_options();
6970 $defaults = Vigilante_Settings::get_defaults_preserving_user_data( $current_options );
6971
6972 // Check if section exists in defaults
6973 if ( ! isset( $defaults[ $section ] ) ) {
6974 wp_send_json_error( __( 'Invalid section.', 'vigilante' ) );
6975 }
6976
6977 $new_values = $defaults[ $section ];
6978
6979 /*
6980 * On a subsite, the settings written to wp-config.php and .htaccess are
6981 * the main site's business. Resetting the local copy of those would only
6982 * make this screen disagree with the file, so they are carried over
6983 * untouched, and a section that is nothing but shared settings is not
6984 * reset at all.
6985 */
6986 if ( ! Vigilante_Settings::can_write_shared_files() ) {
6987 $shared = Vigilante_Settings::get_shared_file_settings();
6988
6989 if ( isset( $shared[ $section ] ) ) {
6990 if ( true === $shared[ $section ] ) {
6991 wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
6992 }
6993
6994 foreach ( $shared[ $section ] as $shared_key ) {
6995 if ( array_key_exists( $shared_key, (array) $current_options[ $section ] ) ) {
6996 $new_values[ $shared_key ] = $current_options[ $section ][ $shared_key ];
6997 }
6998 }
6999 }
7000 }
7001
7002 $current_options[ $section ] = $new_values;
7003
7004 // Save
7005 update_option( Vigilante_Settings::OPTION_NAME, $current_options );
7006 $this->settings->clear_cache();
7007
7008 // Apply file changes if needed
7009 $this->apply_section_changes( $section, $current_options );
7010
7011 wp_send_json_success( array(
7012 'message' => __( 'Section reset to defaults.', 'vigilante' ),
7013 'section' => $section,
7014 'reload' => true,
7015 ) );
7016 }
7017
7018 /**
7019 * AJAX: Clear lockouts
7020 */
7021 public function ajax_clear_lockouts() {
7022 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7023
7024 if ( ! current_user_can( 'manage_options' ) ) {
7025 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7026 }
7027
7028 $ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : '';
7029
7030 if ( ! empty( $ip ) ) {
7031 $this->database->clear_lockout( $ip );
7032 } else {
7033 $this->database->clear_all_lockouts();
7034 }
7035
7036 wp_send_json_success( __( 'Lockouts cleared.', 'vigilante' ) );
7037 }
7038
7039 /**
7040 * AJAX: Clear logs
7041 */
7042 public function ajax_clear_logs() {
7043 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7044
7045 if ( ! current_user_can( 'manage_options' ) ) {
7046 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7047 }
7048
7049 if ( $this->activity_log ) {
7050 $result = $this->activity_log->clear_all_logs();
7051 if ( $result ) {
7052 wp_send_json_success( __( 'Logs cleared.', 'vigilante' ) );
7053 } else {
7054 wp_send_json_error( __( 'Failed to clear logs.', 'vigilante' ) );
7055 }
7056 } else {
7057 wp_send_json_error( __( 'Activity log not available.', 'vigilante' ) );
7058 }
7059 }
7060
7061 /**
7062 * AJAX: Run file integrity scan.
7063 *
7064 * Triggers the file integrity scan, which now also runs the closed plugins
7065 * check at the end when the `check_closed_plugins` toggle is on. Activity
7066 * log is passed through so Security Audit entries (both file-level and
7067 * plugin-status) are recorded from this entry point.
7068 */
7069 public function ajax_run_scan() {
7070 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7071
7072 if ( ! current_user_can( 'manage_options' ) ) {
7073 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7074 }
7075
7076 // Clear previous results before running new scan
7077 delete_option( 'vigilante_last_integrity_results' );
7078 delete_option( 'vigilante_last_integrity_scan' );
7079
7080 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log );
7081 $results = $file_integrity->run_scan();
7082
7083 // Save new results
7084 update_option( 'vigilante_last_integrity_scan', time() );
7085 update_option( 'vigilante_last_integrity_results', $results );
7086
7087 wp_send_json_success( array(
7088 'message' => __( 'Scan completed.', 'vigilante' ),
7089 'results' => $results,
7090 'ignored_count' => count( get_option( 'vigilante_ignored_files', array() ) ),
7091 ) );
7092 }
7093
7094 /**
7095 * AJAX: Clear scan results.
7096 *
7097 * Wipes visually everything inside "Last Scan Results": file scan findings
7098 * (Suspicious / Modified / Extra / Critical Config), file hashes and the
7099 * Closed + Removed Plugins block.
7100 *
7101 * Implementation detail to keep persistence intact:
7102 * - We delete the file scan options + hashes outright.
7103 * - For plugin status we ONLY delete the last_check timestamp — the state
7104 * map and the ignore list are preserved in DB. The render gates the
7105 * plugin_status subsections on last_check > 0, so they hide after
7106 * Clear (visual reset) and reappear on the next Run Scan Now with the
7107 * state intact. This avoids degrading a 'removed' slug (404 without
7108 * metadata) back to 'not_in_repo' on the next scan, which would
7109 * silently lose the alert.
7110 *
7111 * The Ignored Files list and the Ignored Closed + Removed Plugins list
7112 * are preserved on purpose; each has its own explicit "Clear All …"
7113 * button.
7114 */
7115 public function ajax_clear_scan() {
7116 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7117
7118 if ( ! current_user_can( 'manage_options' ) ) {
7119 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7120 }
7121
7122 delete_option( 'vigilante_last_integrity_results' );
7123 delete_option( 'vigilante_last_integrity_scan' );
7124
7125 if ( $this->database ) {
7126 $this->database->clear_file_hashes();
7127 }
7128
7129 // Visual reset of plugin_status block without touching the state map
7130 // or the ignored list. See PHPDoc above for the rationale.
7131 delete_option( 'vigilante_plugin_status_last_check' );
7132
7133 wp_send_json_success( __( 'Scan results cleared.', 'vigilante' ) );
7134 }
7135
7136 /**
7137 * AJAX: Ignore a file from scan results
7138 */
7139 public function ajax_ignore_file() {
7140 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7141
7142 if ( ! current_user_can( 'manage_options' ) ) {
7143 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7144 }
7145
7146 $file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : '';
7147
7148 if ( empty( $file ) ) {
7149 wp_send_json_error( __( 'No file specified.', 'vigilante' ) );
7150 }
7151
7152 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
7153 $file_integrity->ignore_file( $file );
7154
7155 // Also remove the file from stored scan results so UI updates
7156 $results = get_option( 'vigilante_last_integrity_results' );
7157 if ( $results ) {
7158 foreach ( array( 'modified', 'suspicious', 'extra' ) as $category ) {
7159 if ( ! empty( $results[ $category ] ) ) {
7160 $results[ $category ] = array_values(
7161 array_filter(
7162 $results[ $category ],
7163 function ( $item ) use ( $file ) {
7164 return ( is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item ) !== $file;
7165 }
7166 )
7167 );
7168 }
7169 }
7170 update_option( 'vigilante_last_integrity_results', $results );
7171 }
7172
7173 wp_send_json_success( __( 'File added to ignored list.', 'vigilante' ) );
7174 }
7175
7176 /**
7177 * AJAX: Stop ignoring a file
7178 */
7179 public function ajax_unignore_file() {
7180 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7181
7182 if ( ! current_user_can( 'manage_options' ) ) {
7183 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7184 }
7185
7186 $file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : '';
7187
7188 if ( empty( $file ) ) {
7189 wp_send_json_error( __( 'No file specified.', 'vigilante' ) );
7190 }
7191
7192 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
7193 $file_integrity->unignore_file( $file );
7194
7195 wp_send_json_success( __( 'File removed from ignored list.', 'vigilante' ) );
7196 }
7197
7198 /**
7199 * AJAX: Bulk ignore multiple files at once
7200 *
7201 * Processes a single batch into ignored list and prunes them from the
7202 * stored scan results so the UI updates without a re-scan.
7203 */
7204 public function ajax_bulk_ignore_files() {
7205 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7206
7207 if ( ! current_user_can( 'manage_options' ) ) {
7208 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7209 }
7210
7211 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below per-item.
7212 $raw_files = isset( $_POST['files'] ) ? wp_unslash( $_POST['files'] ) : array();
7213 if ( ! is_array( $raw_files ) ) {
7214 wp_send_json_error( __( 'Invalid request.', 'vigilante' ) );
7215 }
7216
7217 $files = array();
7218 foreach ( $raw_files as $f ) {
7219 $clean = sanitize_text_field( $f );
7220 if ( '' !== $clean ) {
7221 $files[] = $clean;
7222 }
7223 }
7224
7225 if ( empty( $files ) ) {
7226 wp_send_json_error( __( 'No files selected.', 'vigilante' ) );
7227 }
7228
7229 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
7230 $count = 0;
7231 foreach ( $files as $file ) {
7232 $file_integrity->ignore_file( $file );
7233 $count++;
7234 }
7235
7236 // Also prune the stored scan results so the UI matches the new ignore list.
7237 $results = get_option( 'vigilante_last_integrity_results' );
7238 if ( $results ) {
7239 $files_set = array_flip( $files );
7240 foreach ( array( 'modified', 'suspicious', 'extra' ) as $category ) {
7241 if ( ! empty( $results[ $category ] ) ) {
7242 $results[ $category ] = array_values(
7243 array_filter(
7244 $results[ $category ],
7245 function ( $item ) use ( $files_set ) {
7246 $path = is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item;
7247 return ! isset( $files_set[ $path ] );
7248 }
7249 )
7250 );
7251 }
7252 }
7253 update_option( 'vigilante_last_integrity_results', $results );
7254 }
7255
7256 wp_send_json_success(
7257 array(
7258 'count' => $count,
7259 'message' => sprintf(
7260 /* translators: %d: number of files added to the ignored list */
7261 _n( '%d file added to ignored list.', '%d files added to ignored list.', $count, 'vigilante' ),
7262 $count
7263 ),
7264 )
7265 );
7266 }
7267
7268 /**
7269 * AJAX: Bulk un-ignore multiple files at once
7270 */
7271 public function ajax_bulk_unignore_files() {
7272 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7273
7274 if ( ! current_user_can( 'manage_options' ) ) {
7275 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7276 }
7277
7278 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below per-item.
7279 $raw_files = isset( $_POST['files'] ) ? wp_unslash( $_POST['files'] ) : array();
7280 if ( ! is_array( $raw_files ) ) {
7281 wp_send_json_error( __( 'Invalid request.', 'vigilante' ) );
7282 }
7283
7284 $files = array();
7285 foreach ( $raw_files as $f ) {
7286 $clean = sanitize_text_field( $f );
7287 if ( '' !== $clean ) {
7288 $files[] = $clean;
7289 }
7290 }
7291
7292 if ( empty( $files ) ) {
7293 wp_send_json_error( __( 'No files selected.', 'vigilante' ) );
7294 }
7295
7296 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
7297 $count = 0;
7298 foreach ( $files as $file ) {
7299 $file_integrity->unignore_file( $file );
7300 $count++;
7301 }
7302
7303 wp_send_json_success(
7304 array(
7305 'count' => $count,
7306 'message' => sprintf(
7307 /* translators: %d: number of files removed from the ignored list */
7308 _n( '%d file removed from ignored list.', '%d files removed from ignored list.', $count, 'vigilante' ),
7309 $count
7310 ),
7311 )
7312 );
7313 }
7314
7315 /**
7316 * AJAX: Clear all ignored files
7317 */
7318 public function ajax_clear_ignored() {
7319 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7320
7321 if ( ! current_user_can( 'manage_options' ) ) {
7322 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7323 }
7324
7325 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
7326 $file_integrity->clear_ignored_files();
7327
7328 wp_send_json_success( __( 'Ignored files list cleared.', 'vigilante' ) );
7329 }
7330
7331 /**
7332 * AJAX: Ignore a closed/removed plugin slug so it stops appearing in the
7333 * main list and email digests. The plugin keeps running on the site;
7334 * silencing is purely cosmetic and reversible.
7335 */
7336 public function ajax_ignore_closed_plugin() {
7337 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7338
7339 if ( ! current_user_can( 'manage_options' ) ) {
7340 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7341 }
7342
7343 $slug = isset( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
7344 if ( '' === $slug ) {
7345 wp_send_json_error( __( 'No slug specified.', 'vigilante' ) );
7346 }
7347
7348 if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) {
7349 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
7350 }
7351 $checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
7352 $checker->ignore_slug( $slug );
7353
7354 wp_send_json_success( __( 'Plugin added to the ignored list.', 'vigilante' ) );
7355 }
7356
7357 /**
7358 * AJAX: Stop ignoring a previously-ignored closed/removed plugin slug.
7359 */
7360 public function ajax_unignore_closed_plugin() {
7361 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7362
7363 if ( ! current_user_can( 'manage_options' ) ) {
7364 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7365 }
7366
7367 $slug = isset( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
7368 if ( '' === $slug ) {
7369 wp_send_json_error( __( 'No slug specified.', 'vigilante' ) );
7370 }
7371
7372 if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) {
7373 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
7374 }
7375 $checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
7376 $checker->unignore_slug( $slug );
7377
7378 wp_send_json_success( __( 'Plugin removed from the ignored list.', 'vigilante' ) );
7379 }
7380
7381 /**
7382 * AJAX: Clear the entire ignored-closed-plugins list.
7383 */
7384 public function ajax_clear_ignored_closed_plugins() {
7385 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7386
7387 if ( ! current_user_can( 'manage_options' ) ) {
7388 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7389 }
7390
7391 if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) {
7392 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
7393 }
7394 $checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
7395 $checker->clear_ignored();
7396
7397 wp_send_json_success( __( 'Ignored closed plugins list cleared.', 'vigilante' ) );
7398 }
7399
7400 /**
7401 * AJAX: Test security headers
7402 */
7403 public function ajax_test_headers() {
7404 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7405
7406 if ( ! current_user_can( 'manage_options' ) ) {
7407 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7408 }
7409
7410 // Get security grade from settings (not from actual HTTP request)
7411 $security_headers = new Vigilante_Security_Headers( $this->settings );
7412 $results = $security_headers->get_security_grade();
7413
7414 wp_send_json_success( $results );
7415 }
7416
7417 /**
7418 * Sanitize section data (kept for compatibility)
7419 *
7420 * @param string $section Section name.
7421 * @param array $data Section data.
7422 * @return array
7423 */
7424 private function sanitize_section_data( $section, $data ) {
7425 $sanitized = array();
7426
7427 foreach ( $data as $key => $value ) {
7428 $key = sanitize_key( $key );
7429
7430 if ( is_array( $value ) ) {
7431 $sanitized[ $key ] = $this->sanitize_section_data( $key, $value );
7432 } elseif ( is_numeric( $value ) ) {
7433 $sanitized[ $key ] = intval( $value );
7434 } else {
7435 $sanitized[ $key ] = sanitize_text_field( $value );
7436 }
7437 }
7438
7439 return $sanitized;
7440 }
7441
7442 /**
7443 * Apply changes after saving settings
7444 *
7445 * @param string $section Section that was updated.
7446 * @param array $all_options All options.
7447 */
7448 private function apply_section_changes( $section, $all_options ) {
7449 // Create fresh settings instance to ensure we have the latest data
7450 $fresh_settings = new Vigilante_Settings();
7451
7452 // The shared Vigilant .htaccess block carries BOTH the firewall's
7453 // file-protection rules and the server-signature / fingerprinting rules
7454 // that are configured under Security Headers. So it must be regenerated
7455 // whenever either section (or the module toggles) changes, not only on
7456 // the firewall save — otherwise toggling "Hide server signature" or
7457 // "Remove fingerprinting headers" never reaches the .htaccess.
7458 if ( in_array( $section, array( 'firewall', 'security_headers', 'modules' ), true ) ) {
7459 $htaccess = new Vigilante_Htaccess_Protection( $fresh_settings );
7460 $sh = $fresh_settings->get_section( 'security_headers' );
7461 $needs_htaccess_block = ! empty( $all_options['modules']['firewall'] )
7462 || ! empty( $sh['hide_server_signature'] )
7463 || ! empty( $sh['remove_fingerprinting_headers'] );
7464
7465 if ( $needs_htaccess_block ) {
7466 $htaccess->apply_rules();
7467 } else {
7468 $htaccess->remove_rules();
7469 }
7470 }
7471
7472 // Regenerate the HTTP security headers (sent by PHP) for the headers section.
7473 if ( 'security_headers' === $section || 'modules' === $section ) {
7474 $security_headers = new Vigilante_Security_Headers( $fresh_settings );
7475 $headers_enabled = ! empty( $all_options['modules']['security_headers'] );
7476
7477 if ( $headers_enabled ) {
7478 $security_headers->apply_rules();
7479 } else {
7480 $security_headers->remove_rules();
7481 }
7482 }
7483
7484 // Regenerate wp-config for wp_hardening section
7485 if ( 'wp_hardening' === $section || 'modules' === $section ) {
7486 $wpconfig = new Vigilante_Wpconfig_Security( $fresh_settings );
7487 $hardening_enabled = ! empty( $all_options['modules']['wp_hardening'] );
7488
7489 if ( $hardening_enabled ) {
7490 $wpconfig->apply_security_constants();
7491 } else {
7492 $wpconfig->remove_constants();
7493 }
7494
7495 // Apply WordPress options for comments/pingbacks
7496 $hardening_options = $all_options['wp_hardening'] ?? array();
7497
7498 if ( $hardening_enabled ) {
7499 // Pingbacks
7500 if ( ! empty( $hardening_options['disable_pingbacks'] ) ) {
7501 update_option( 'default_pingback_flag', 0 );
7502 } else {
7503 // Restore default: pingbacks enabled
7504 update_option( 'default_pingback_flag', 1 );
7505 }
7506
7507 // Trackbacks and ping status
7508 // Only close if either pingbacks OR trackbacks are disabled
7509 if ( ! empty( $hardening_options['disable_pingbacks'] ) || ! empty( $hardening_options['disable_trackbacks'] ) ) {
7510 update_option( 'default_ping_status', 'closed' );
7511 } else {
7512 // Restore default: pings open
7513 update_option( 'default_ping_status', 'open' );
7514 }
7515
7516 // Comment moderation
7517 if ( ! empty( $hardening_options['require_comment_moderation'] ) ) {
7518 update_option( 'comment_moderation', 1 );
7519 } else {
7520 // Restore default: no moderation required
7521 update_option( 'comment_moderation', 0 );
7522 }
7523 }
7524 }
7525
7526 // Trim activity log entries immediately when limits change
7527 if ( 'activity_log' === $section && $this->activity_log ) {
7528 $this->activity_log->cleanup_old_logs();
7529 }
7530
7531 // Flush rewrite rules if login URL changed
7532 if ( 'login_security' === $section ) {
7533 $login_options = $all_options['login_security'] ?? array();
7534 if ( ! empty( $login_options['custom_login_url'] ) ) {
7535 delete_option( 'vigilante_login_rules_version' );
7536 }
7537 }
7538
7539 // Log the settings change with readable section name
7540 if ( $this->activity_log ) {
7541 $section_names = array(
7542 'firewall' => __( 'Firewall', 'vigilante' ),
7543 'login_security' => __( 'Login Security', 'vigilante' ),
7544 'security_headers' => __( 'Security Headers', 'vigilante' ),
7545 'rest_api_security'=> __( 'REST API Security', 'vigilante' ),
7546 'user_security' => __( 'User Security', 'vigilante' ),
7547 'wp_hardening' => __( 'WP Hardening', 'vigilante' ),
7548 'activity_log' => __( 'Security Audit', 'vigilante' ),
7549 'file_integrity' => __( 'File Integrity', 'vigilante' ),
7550 'email' => __( 'Notification Settings', 'vigilante' ),
7551 'backup' => __( 'Backup', 'vigilante' ),
7552 'advanced' => __( 'Advanced', 'vigilante' ),
7553 'modules' => __( 'Modules', 'vigilante' ),
7554 );
7555 $display_name = isset( $section_names[ $section ] ) ? $section_names[ $section ] : $section;
7556
7557 $this->activity_log->log(
7558 'settings',
7559 'settings_updated',
7560 sprintf(
7561 /* translators: %s: Section name */
7562 __( 'Settings updated: %s', 'vigilante' ),
7563 $display_name
7564 ),
7565 array( 'section' => $section ),
7566 'info'
7567 );
7568 }
7569 }
7570
7571 /**
7572 * Apply all file changes (htaccess, wp-config) based on current options
7573 *
7574 * Used after preset, import, or reset operations
7575 *
7576 * @param array $all_options All plugin options.
7577 */
7578 private function apply_all_file_changes( $all_options ) {
7579 // Refresh settings cache first
7580 $this->settings->clear_cache();
7581
7582 // Create fresh settings instance
7583 $fresh_settings = new Vigilante_Settings();
7584
7585 // Apply firewall htaccess changes
7586 $htaccess = new Vigilante_Htaccess_Protection( $fresh_settings );
7587 $firewall_enabled = ! empty( $all_options['modules']['firewall'] );
7588
7589 if ( $firewall_enabled ) {
7590 $htaccess->apply_rules();
7591 } else {
7592 $htaccess->remove_rules();
7593 }
7594
7595 // Apply security headers htaccess changes
7596 $security_headers = new Vigilante_Security_Headers( $fresh_settings );
7597 $headers_enabled = ! empty( $all_options['modules']['security_headers'] );
7598
7599 if ( $headers_enabled ) {
7600 $security_headers->apply_rules();
7601 } else {
7602 $security_headers->remove_rules();
7603 }
7604
7605 // Apply wp-config changes
7606 $wpconfig = new Vigilante_Wpconfig_Security( $fresh_settings );
7607 $hardening_enabled = ! empty( $all_options['modules']['wp_hardening'] );
7608
7609 if ( $hardening_enabled ) {
7610 $wpconfig->apply_security_constants();
7611 } else {
7612 $wpconfig->remove_constants();
7613 }
7614
7615 // Apply WordPress options for comments/pingbacks
7616 $hardening_options = $all_options['wp_hardening'] ?? array();
7617
7618 if ( $hardening_enabled ) {
7619 // Pingbacks
7620 if ( ! empty( $hardening_options['disable_pingbacks'] ) ) {
7621 update_option( 'default_pingback_flag', 0 );
7622 } else {
7623 // Restore default: pingbacks enabled
7624 update_option( 'default_pingback_flag', 1 );
7625 }
7626
7627 // Trackbacks and ping status
7628 // Only close if either pingbacks OR trackbacks are disabled
7629 if ( ! empty( $hardening_options['disable_pingbacks'] ) || ! empty( $hardening_options['disable_trackbacks'] ) ) {
7630 update_option( 'default_ping_status', 'closed' );
7631 } else {
7632 // Restore default: pings open
7633 update_option( 'default_ping_status', 'open' );
7634 }
7635
7636 // Comment moderation
7637 if ( ! empty( $hardening_options['require_comment_moderation'] ) ) {
7638 update_option( 'comment_moderation', 1 );
7639 } else {
7640 // Restore default: no moderation required
7641 update_option( 'comment_moderation', 0 );
7642 }
7643 }
7644
7645 // Log the change
7646 if ( $this->activity_log ) {
7647 $this->activity_log->log(
7648 'settings',
7649 'bulk_settings_applied',
7650 __( 'Bulk settings applied (preset/import/reset)', 'vigilante' ),
7651 array(),
7652 'info'
7653 );
7654 }
7655 }
7656 }