PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.9
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.9
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 2.9.4 2.9.3 All 86 releases
vigilante / admin / class-admin.php

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

8,385 lines 537.2 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 use Vigilante_Admin_Recovery_Ajax;
32
33 /**
34 * Settings instance
35 *
36 * @var Vigilante_Settings
37 */
38 private $settings;
39
40 /**
41 * Database instance
42 *
43 * @var Vigilante_Database
44 */
45 private $database;
46
47 /**
48 * Activity log instance
49 *
50 * @var Vigilante_Activity_Log
51 */
52 private $activity_log;
53
54 /**
55 * Current tab
56 *
57 * @var string
58 */
59 private $current_tab = 'dashboard';
60
61 /**
62 * Available tabs
63 *
64 * @var array
65 */
66 private $tabs = array();
67
68 /**
69 * Constructor
70 *
71 * @param Vigilante_Settings $settings Settings instance.
72 * @param Vigilante_Database $database Database instance.
73 * @param Vigilante_Activity_Log $activity_log Activity log instance.
74 */
75 public function __construct( $settings, $database, $activity_log ) {
76 $this->settings = $settings;
77 $this->database = $database;
78 $this->activity_log = $activity_log;
79
80 $this->setup_tabs();
81 $this->init_hooks();
82 }
83
84 /**
85 * Setup available tabs
86 */
87 private function setup_tabs() {
88 $this->tabs = array(
89 'dashboard' => __( 'Dashboard', 'vigilante' ),
90 'firewall' => __( 'Firewall', 'vigilante' ),
91 'headers' => __( 'Security Headers', 'vigilante' ),
92 'login' => __( 'Login Security', 'vigilante' ),
93 'rest-api' => __( 'REST API', 'vigilante' ),
94 'users' => __( 'User Security', 'vigilante' ),
95 'wp-hardening' => __( 'WP Hardening', 'vigilante' ),
96 'file-integrity' => __( 'File Integrity', 'vigilante' ),
97 'activity-log' => __( 'Security Audit', 'vigilante' ),
98 'tools' => __( 'Settings & Tools', 'vigilante' ),
99 );
100 }
101
102 /**
103 * Initialize hooks
104 */
105 private function init_hooks() {
106 add_action( 'admin_menu', array( $this, 'add_menu' ) );
107 add_action( 'admin_init', array( $this, 'redirect_submenu_shortcuts' ) );
108 add_action( 'admin_init', array( $this, 'register_settings' ) );
109 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
110 add_action( 'admin_notices', array( $this, 'show_admin_notices' ) );
111
112 // Highlight correct submenu based on active tab
113 add_filter( 'submenu_file', array( $this, 'highlight_submenu_tab' ) );
114
115 // Set browser tab title to show plugin name and active tab
116 add_filter( 'admin_title', array( $this, 'set_admin_page_title' ), 10, 2 );
117
118 // AJAX handlers
119 add_action( 'wp_ajax_vigilante_save_settings', array( $this, 'ajax_save_settings' ) );
120 add_action( 'wp_ajax_vigilante_apply_preset', array( $this, 'ajax_apply_preset' ) );
121 add_action( 'wp_ajax_vigilante_reset_section', array( $this, 'ajax_reset_section' ) );
122 add_action( 'wp_ajax_vigilante_clear_lockouts', array( $this, 'ajax_clear_lockouts' ) );
123 add_action( 'wp_ajax_vigilante_clear_logs', array( $this, 'ajax_clear_logs' ) );
124 add_action( 'wp_ajax_vigilante_run_scan', array( $this, 'ajax_run_scan' ) );
125 add_action( 'wp_ajax_vigilante_clear_scan', array( $this, 'ajax_clear_scan' ) );
126 add_action( 'wp_ajax_vigilante_ignore_file', array( $this, 'ajax_ignore_file' ) );
127 add_action( 'wp_ajax_vigilante_unignore_file', array( $this, 'ajax_unignore_file' ) );
128 add_action( 'wp_ajax_vigilante_bulk_ignore_files', array( $this, 'ajax_bulk_ignore_files' ) );
129 add_action( 'wp_ajax_vigilante_bulk_unignore_files', array( $this, 'ajax_bulk_unignore_files' ) );
130 add_action( 'wp_ajax_vigilante_clear_ignored', array( $this, 'ajax_clear_ignored' ) );
131 add_action( 'wp_ajax_vigilante_ignore_closed_plugin', array( $this, 'ajax_ignore_closed_plugin' ) );
132 add_action( 'wp_ajax_vigilante_unignore_closed_plugin', array( $this, 'ajax_unignore_closed_plugin' ) );
133 add_action( 'wp_ajax_vigilante_clear_ignored_closed_plugins', array( $this, 'ajax_clear_ignored_closed_plugins' ) );
134 add_action( 'wp_ajax_vigilante_approve_critical_file', array( $this, 'ajax_approve_critical_file' ) );
135 add_action( 'wp_ajax_vigilante_export_settings', array( $this, 'ajax_export_settings' ) );
136 add_action( 'wp_ajax_vigilante_import_settings', array( $this, 'ajax_import_settings' ) );
137 add_action( 'wp_ajax_vigilante_get_logs', array( $this, 'ajax_get_logs' ) );
138 add_action( 'wp_ajax_vigilante_test_headers', array( $this, 'ajax_test_headers' ) );
139 add_action( 'wp_ajax_vigilante_download_files_backup', array( $this, 'ajax_download_files_backup' ) );
140
141 // 2FA AJAX handlers
142 add_action( 'wp_ajax_vigilante_search_users_2fa', array( $this, 'ajax_search_users_2fa' ) );
143 add_action( 'wp_ajax_vigilante_send_2fa_notification', array( $this, 'ajax_send_2fa_notification' ) );
144 add_action( 'wp_ajax_vigilante_search_totp_users', array( $this, 'ajax_search_totp_users' ) );
145 add_action( 'wp_ajax_vigilante_reset_totp_users', array( $this, 'ajax_reset_totp_users' ) );
146 add_action( 'wp_ajax_vigilante_totp_get_setup', array( $this, 'ajax_totp_get_setup' ) );
147 add_action( 'wp_ajax_vigilante_notify_login_url', array( $this, 'ajax_notify_login_url' ) );
148
149 // Password Reset AJAX handlers
150 add_action( 'wp_ajax_vigilante_search_users_password_reset', array( $this, 'ajax_search_users_password_reset' ) );
151 add_action( 'wp_ajax_vigilante_force_password_reset', array( $this, 'ajax_force_password_reset' ) );
152 add_action( 'wp_ajax_vigilante_force_password_reset_all', array( $this, 'ajax_force_password_reset_all' ) );
153 add_action( 'wp_ajax_vigilante_force_password_reset_by_role', array( $this, 'ajax_force_password_reset_by_role' ) );
154
155 // User approval AJAX handlers
156 add_action( 'wp_ajax_vigilante_approve_user', array( $this, 'ajax_approve_user' ) );
157 add_action( 'wp_ajax_vigilante_reject_user', array( $this, 'ajax_reject_user' ) );
158
159 // Session management AJAX handlers
160 add_action( 'wp_ajax_vigilante_get_user_sessions', array( $this, 'ajax_get_user_sessions' ) );
161 add_action( 'wp_ajax_vigilante_revoke_session', array( $this, 'ajax_revoke_session' ) );
162 add_action( 'wp_ajax_vigilante_revoke_all_sessions', array( $this, 'ajax_revoke_all_sessions' ) );
163
164 // Under Attack mode AJAX handlers
165 add_action( 'wp_ajax_vigilante_activate_under_attack', array( $this, 'ajax_activate_under_attack' ) );
166 add_action( 'wp_ajax_vigilante_deactivate_under_attack', array( $this, 'ajax_deactivate_under_attack' ) );
167 add_action( 'wp_ajax_vigilante_under_attack_status', array( $this, 'ajax_under_attack_status' ) );
168
169 // Database backup AJAX handlers
170 add_action( 'wp_ajax_vigilante_get_db_tables', array( $this, 'ajax_get_db_tables' ) );
171 add_action( 'wp_ajax_vigilante_download_db_backup', array( $this, 'ajax_download_db_backup' ) );
172
173 // Database prefix AJAX handlers
174 add_action( 'wp_ajax_vigilante_generate_prefix', array( $this, 'ajax_generate_prefix' ) );
175 add_action( 'wp_ajax_vigilante_change_prefix', array( $this, 'ajax_change_prefix' ) );
176
177 // Firewall list management from activity log popup
178 add_action( 'wp_ajax_vigilante_add_to_firewall_list', array( $this, 'ajax_add_to_firewall_list' ) );
179 add_action( 'wp_ajax_vigilante_unblock_firewall_ip', array( $this, 'ajax_unblock_firewall_ip' ) );
180
181 // Security Analyzer AJAX handlers (v2.1.0)
182 add_action( 'wp_ajax_vigilante_analyzer_run', array( $this, 'ajax_analyzer_run' ) );
183 add_action( 'wp_ajax_vigilante_analyzer_history', array( $this, 'ajax_analyzer_history' ) );
184 add_action( 'wp_ajax_vigilante_analyzer_dismiss_notice', array( $this, 'ajax_analyzer_dismiss_notice' ) );
185 add_action( 'wp_ajax_vigilante_analyzer_save_settings', array( $this, 'ajax_analyzer_save_settings' ) );
186
187 // Security Headers settings recovery (2.10.0)
188 add_action( 'wp_ajax_vigilante_headers_recovery_restore', array( $this, 'ajax_headers_recovery_restore' ) );
189 add_action( 'wp_ajax_vigilante_headers_recovery_undo', array( $this, 'ajax_headers_recovery_undo' ) );
190 add_action( 'wp_ajax_vigilante_headers_recovery_dismiss', array( $this, 'ajax_headers_recovery_dismiss' ) );
191
192 // Shared "Send test email" handler — Notification settings, File Integrity, Audit Alerts (v2.8.0)
193 add_action( 'wp_ajax_vigilante_send_test_email', array( $this, 'ajax_send_test_email' ) );
194
195 // Run migrations on admin load
196 add_action( 'admin_init', array( $this, 'run_migrations' ) );
197 }
198
199 /**
200 * Run database migrations based on stored version
201 */
202 public function run_migrations() {
203 $db_version = get_option( 'vigilante_db_version', '0' );
204
205 // 1.2.3: Fix IP lists corrupted by sanitize_text_field stripping newlines
206 if ( version_compare( $db_version, '1.2.3', '<' ) ) {
207 $this->migrate_fix_ip_lists();
208 update_option( 'vigilante_db_version', '1.2.3' );
209 }
210
211 // 1.3.0: Add request_method column to activity log table
212 if ( version_compare( $db_version, '1.3.0', '<' ) ) {
213 $this->database->run_migrations();
214 }
215
216 // 1.9.0: Re-apply wp-config constants (performance constants removed from managed list)
217 if ( version_compare( $db_version, '1.9.0', '<' ) ) {
218 if ( $this->settings->is_module_enabled( 'wp_hardening' ) ) {
219 require_once VIGILANTE_INCLUDES_DIR . 'class-wpconfig-security.php';
220 $wpconfig = new Vigilante_Wpconfig_Security( $this->settings );
221 $wpconfig->apply_security_constants();
222 }
223 update_option( 'vigilante_db_version', '1.9.0' );
224 }
225
226 // 1.10.0: Clean up orphaned email fields (centralized notification recipients)
227 if ( version_compare( $db_version, '1.10.0', '<' ) ) {
228 $this->migrate_cleanup_email_fields();
229 update_option( 'vigilante_db_version', '1.10.0' );
230 }
231
232 // 1.11.0: Remove stale 'enabled' key from activity_log settings
233 // + Convert additional_recipients from string to array
234 if ( version_compare( $db_version, '1.11.0', '<' ) ) {
235 $options = get_option( Vigilante_Settings::OPTION_NAME, array() );
236 $changed = false;
237
238 if ( isset( $options['activity_log']['enabled'] ) ) {
239 unset( $options['activity_log']['enabled'] );
240 $changed = true;
241 }
242
243 // Convert corrupted string to array for additional_recipients
244 if ( isset( $options['email']['additional_recipients'] ) && is_string( $options['email']['additional_recipients'] ) ) {
245 $raw = trim( $options['email']['additional_recipients'] );
246 if ( ! empty( $raw ) ) {
247 $emails = array_filter( array_map( 'trim', preg_split( '/[\r\n,; ]+/', $raw ) ) );
248 $options['email']['additional_recipients'] = array_values( array_filter( $emails, 'is_email' ) );
249 } else {
250 $options['email']['additional_recipients'] = array();
251 }
252 $changed = true;
253 }
254
255 if ( $changed ) {
256 update_option( Vigilante_Settings::OPTION_NAME, $options );
257 }
258 update_option( 'vigilante_db_version', '1.11.0' );
259 }
260
261 // 1.12.1: Regenerate htaccess (WooCommerce IPN exclusion in bot blocking rule)
262 if ( version_compare( $db_version, '1.12.1', '<' ) ) {
263 if ( ! empty( $this->settings->get_section( 'firewall' )['block_bad_bots'] ) ) {
264 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php';
265 $htaccess = new Vigilante_Htaccess_Protection( $this->settings );
266 $htaccess->apply_rules();
267 }
268 update_option( 'vigilante_db_version', '1.12.1' );
269 }
270
271 // 1.14.0: Generate critical config files baseline (wp-config.php, .htaccess)
272 if ( version_compare( $db_version, '1.14.0', '<' ) ) {
273 if ( ! class_exists( 'Vigilante_File_Integrity' ) ) {
274 require_once VIGILANTE_INCLUDES_DIR . 'class-file-integrity.php';
275 }
276 $fi = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log );
277
278 /*
279 * Only when there is nothing on record. This migration exists to
280 * create the baseline that did not exist, never to discard the one
281 * the owner approved: rebuilding it from the files takes whatever
282 * is on disk right now as approved, so a wp-config.php modified and
283 * awaiting review would be blessed in silence.
284 *
285 * And this is not theory. vigilante_db_version is written on two
286 * different scales into the same option: this file counts in plugin
287 * versions (2.11.0) and Vigilante_Database counts in schema
288 * versions, currently 1.4.0 (class-database.php:322 and :380). For
289 * version_compare, 1.4.0 is LOWER than 1.14.0, so any site whose
290 * option was last written by the schema runs this migration again.
291 * Measured on the Multisite install on 10 sep 2026: one of the three
292 * sites was sitting on 1.4.0.
293 */
294 if ( ! $fi->get_critical_files_baseline() ) {
295 $fi->regenerate_all_baselines();
296 }
297
298 update_option( 'vigilante_db_version', '1.14.0' );
299 }
300
301 // 2.0.0: Move hide_server_signature and remove_fingerprinting_headers
302 // from firewall section to security_headers section
303 if ( version_compare( $db_version, '2.0.0', '<' ) ) {
304 $options = get_option( Vigilante_Settings::OPTION_NAME, array() );
305 $changed = false;
306
307 foreach ( array( 'hide_server_signature', 'remove_fingerprinting_headers' ) as $key ) {
308 if ( isset( $options['firewall'][ $key ] ) ) {
309 if ( ! isset( $options['security_headers'][ $key ] ) ) {
310 $options['security_headers'][ $key ] = $options['firewall'][ $key ];
311 }
312 unset( $options['firewall'][ $key ] );
313 $changed = true;
314 }
315 }
316
317 if ( $changed ) {
318 update_option( Vigilante_Settings::OPTION_NAME, $options );
319 }
320 update_option( 'vigilante_db_version', '2.0.0' );
321 }
322
323 // 2.6.1: Two things happen here.
324 //
325 // 1. Re-apply wp-config constants so the block is rewritten with
326 // "if ( ! defined() )" guards around every define(). Without guards,
327 // non-standard setups that pre-define WordPress constants outside
328 // wp-config.php (custom bootstraps that load constants from .env or
329 // similar) hit a fatal "Constant already defined" when wp-config.php
330 // is parsed and reaches our block.
331 //
332 // 2. Drop the cached Security Check report. The cached "max" per
333 // category was frozen at scan time; with the internal category
334 // bumping from 22 to 28 points (closed_plugins added in 2.6.0),
335 // the cached report would keep displaying 22/22 until the next
336 // full scan. Clearing it forces a fresh scan with the new caps.
337 if ( version_compare( $db_version, '2.6.1', '<' ) ) {
338 if ( $this->settings->is_module_enabled( 'wp_hardening' ) ) {
339 require_once VIGILANTE_INCLUDES_DIR . 'class-wpconfig-security.php';
340 $wpconfig = new Vigilante_Wpconfig_Security( $this->settings );
341 $wpconfig->apply_security_constants();
342 }
343 delete_option( 'vigilante_analyzer_last_scan' );
344
345 // Schedule an immediate background scan so the dashboard widget
346 // doesn't display "Last scan: never" right after the upgrade.
347 // Reuses the same hook the post-Under-Attack flow uses.
348 if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) {
349 wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' );
350 }
351
352 update_option( 'vigilante_db_version', '2.6.1' );
353 }
354
355 // 2.9.3: Regenerate the .htaccess protection block. The bad-bots
356 // User-Agent list dropped substring-prone tokens that 403'd
357 // legitimate clients (e.g. "rma" matched inside "Performance" and
358 // blocked WP Rocket's page fetch), and the blocking rules now honour
359 // the firewall IP / User-Agent whitelists as negated exceptions.
360 // Existing sites only rewrite the block when Server Protection is
361 // saved, so the upgrade has to refresh it once itself (same pattern
362 // as the 1.12.1 WooCommerce IPN migration).
363 if ( version_compare( $db_version, '2.9.3', '<' ) ) {
364 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php';
365 $htaccess = new Vigilante_Htaccess_Protection( $this->settings );
366
367 if ( $htaccess->are_rules_active() ) {
368 $htaccess->apply_rules();
369 }
370
371 update_option( 'vigilante_db_version', '2.9.3' );
372 }
373
374 /*
375 * 2.9.8: the mixed content handling changes shape. "Upgrade Insecure
376 * Requests" becomes a setting of its own, and Fix Mixed Content ships
377 * off, where before it shipped on and carried the directive with it.
378 * Both have to be written down for sites that are updating, so their
379 * pages keep loading exactly what they loaded yesterday.
380 *
381 * Read the RAW stored options, not get_section(): that one merges the
382 * defaults, so a site that never stored the key would be read with the
383 * new default and silently lose the behaviour it had. Absent means the
384 * site was running on the old default, which was on.
385 */
386 if ( version_compare( $db_version, '2.9.8', '<' ) ) {
387 $raw = get_option( Vigilante_Settings::OPTION_NAME, array() );
388 $stored = ( is_array( $raw ) && isset( $raw['security_headers'] ) && is_array( $raw['security_headers'] ) ) ? $raw['security_headers'] : array();
389 $had_fix = array_key_exists( 'fix_mixed_content', $stored ) ? ! empty( $stored['fix_mixed_content'] ) : true;
390
391 /*
392 * Merge, never replace. update_section() overwrites the whole
393 * section, so passing just these two keys wiped every other header
394 * setting the site had stored (HSTS, CSP, cross-origin policies,
395 * the HTTPS switches, Server Identity) and left the screen showing
396 * factory defaults while the .htaccess kept serving the old values.
397 */
398 $this->settings->update_section(
399 'security_headers',
400 array_merge(
401 $stored,
402 array(
403 'fix_mixed_content' => $had_fix,
404 'upgrade_insecure_requests' => $had_fix,
405 )
406 )
407 );
408
409 update_option( 'vigilante_db_version', '2.9.8' );
410 }
411
412 /*
413 * 2.9.9: drop the settings that no code has read for versions.
414 *
415 * They were carried in the defaults and therefore written into every
416 * saved configuration, they show up in an exported configuration, and
417 * anyone reading them assumes a feature exists behind them. Removing
418 * them from the defaults is not enough: the stored copies survive, so
419 * they are swept here too. Nothing reads them, so nothing changes.
420 */
421 if ( version_compare( $db_version, '2.9.9', '<' ) ) {
422 $raw = get_option( Vigilante_Settings::OPTION_NAME, array() );
423 $dead = array(
424 'firewall' => array( 'country_blocking', 'protected_file_extensions' ),
425 'file_integrity' => array( 'suspicious_patterns' ),
426 'backup' => array( 'auto_backup', 'backup_before_update' ),
427 'advanced' => array( 'block_author_archives', 'disable_embeds', 'uninstall_cleanup', 'debug_mode' ),
428 );
429
430 $changed = false;
431 foreach ( $dead as $section => $keys ) {
432 if ( ! isset( $raw[ $section ] ) || ! is_array( $raw[ $section ] ) ) {
433 continue;
434 }
435 foreach ( $keys as $key ) {
436 if ( array_key_exists( $key, $raw[ $section ] ) ) {
437 unset( $raw[ $section ][ $key ] );
438 $changed = true;
439 }
440 }
441 }
442
443 if ( $changed ) {
444 update_option( Vigilante_Settings::OPTION_NAME, $raw );
445 }
446
447 update_option( 'vigilante_db_version', '2.9.9' );
448 }
449
450 /*
451 * 2.11.0: security release (audit of 28 Aug 2026). Runs here and not
452 * from Vigilante_Database::needs_update(): this option is shared with
453 * that class, and on any updated site it already holds a plugin version
454 * (2.9.9 or later), so a bump of DB_VERSION would never fire.
455 * create_tables() widens the email code column through dbDelta (varchar
456 * 6 to 64, the code is stored hashed since 2.11.0) and purge_for_2_11_0()
457 * does what dbDelta cannot: it empties the trusted devices, which were
458 * identified by User-Agent until now (S1), and the pending email codes,
459 * stored in clear until now (S11). Every remembered device asks for the
460 * second factor once more after this update, and the changelog says so.
461 */
462 if ( version_compare( $db_version, '2.11.0', '<' ) ) {
463 $this->database->create_tables();
464 $this->database->purge_for_2_11_0();
465
466 update_option( 'vigilante_db_version', '2.11.0' );
467 }
468
469 /*
470 * 2.11.9: clear the raw .htaccess copies that older versions left in
471 * options, on the first admin load after the update. Uninstall already
472 * removes them, but that only fires when the plugin is deleted, so a
473 * site that keeps the plugin carried them until now. Three stores, each
474 * a copy of a file that can hold secrets (a SetEnv token, an
475 * Authorization header): the same exposure the wp.org review flagged as
476 * 4.4, on the paths its fix did not reach.
477 *
478 * - vigilante_htaccess_history: up to five raw copies, by design, until
479 * 2.11.8. The writer is gone, nothing reads it, so it is deleted.
480 * - vigilante_htaccess_backup: the single rollback buffer, normally
481 * cleared in the finally of each write; a copy only lingers if a write
482 * crashed mid-operation. Nothing outside one write reads it, so a
483 * leftover is deleted.
484 * - vigilante_htaccess_pre_migration: still read by the header recovery,
485 * but older versions stored the whole file where only our own block is
486 * ever used. Truncated to that block, so the feature keeps working and
487 * nothing outside our markers stays in the option.
488 */
489 if ( version_compare( $db_version, '2.11.9', '<' ) ) {
490 delete_option( 'vigilante_htaccess_history' );
491 delete_option( 'vigilante_htaccess_backup' );
492
493 $snapshot = get_option( 'vigilante_htaccess_pre_migration' );
494 if ( is_array( $snapshot ) && isset( $snapshot['content'] ) && '' !== (string) $snapshot['content'] ) {
495 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-recovery.php';
496 $block = Vigilante_Htaccess_Recovery::get_raw_block();
497
498 if ( '' === $block ) {
499 delete_option( 'vigilante_htaccess_pre_migration' );
500 } elseif ( $block !== $snapshot['content'] ) {
501 $snapshot['content'] = $block;
502 update_option( 'vigilante_htaccess_pre_migration', $snapshot, false );
503 }
504 }
505
506 update_option( 'vigilante_db_version', '2.11.9' );
507 }
508 }
509
510 /**
511 * Migration: Remove orphaned email fields from saved options
512 *
513 * v1.10.0 centralized notification recipients into email section.
514 * Old per-module notify_email fields and dead email section fields
515 * are removed to avoid confusion.
516 */
517 private function migrate_cleanup_email_fields() {
518 $options = get_option( Vigilante_Settings::OPTION_NAME, array() );
519 $modified = false;
520
521 // Remove orphaned fields from email section
522 $dead_email_keys = array( 'enabled', 'from_name', 'from_email', 'admin_email', 'send_activation_email', 'custom_email' );
523 if ( isset( $options['email'] ) && is_array( $options['email'] ) ) {
524 foreach ( $dead_email_keys as $key ) {
525 if ( array_key_exists( $key, $options['email'] ) ) {
526 unset( $options['email'][ $key ] );
527 $modified = true;
528 }
529 }
530 // Ensure new fields exist with defaults
531 if ( ! array_key_exists( 'send_to_admin_email', $options['email'] ) ) {
532 $options['email']['send_to_admin_email'] = true;
533 $modified = true;
534 }
535 if ( ! array_key_exists( 'additional_recipients', $options['email'] ) ) {
536 $options['email']['additional_recipients'] = '';
537 $modified = true;
538 }
539 }
540
541 // Remove notify_email from login_security
542 if ( isset( $options['login_security']['notify_email'] ) ) {
543 unset( $options['login_security']['notify_email'] );
544 $modified = true;
545 }
546
547 // Remove notify_email from file_integrity
548 if ( isset( $options['file_integrity']['notify_email'] ) ) {
549 unset( $options['file_integrity']['notify_email'] );
550 $modified = true;
551 }
552
553 if ( $modified ) {
554 update_option( Vigilante_Settings::OPTION_NAME, $options );
555 // Clear settings cache so the plugin uses clean data immediately
556 $this->settings->clear_cache();
557 }
558 }
559
560 /**
561 * Migration: Fix IP whitelist/blacklist entries merged into single line
562 *
563 * Prior to 1.2.3, sanitize_text_field() stripped newlines from textarea data,
564 * causing multiple IPs to be stored as a single space-separated string.
565 */
566 private function migrate_fix_ip_lists() {
567 $options = get_option( Vigilante_Settings::OPTION_NAME, array() );
568 $fixed = false;
569
570 foreach ( array( 'ip_whitelist', 'ip_blacklist' ) as $key ) {
571 if ( ! empty( $options['firewall'][ $key ] ) && is_array( $options['firewall'][ $key ] ) ) {
572 $new_list = array();
573 foreach ( $options['firewall'][ $key ] as $entry ) {
574 // Split entries that were joined by spaces
575 $parts = preg_split( '/\s+/', trim( $entry ) );
576 foreach ( $parts as $part ) {
577 $part = trim( $part );
578 if ( '' !== $part ) {
579 $new_list[] = $part;
580 }
581 }
582 }
583 if ( count( $new_list ) !== count( $options['firewall'][ $key ] ) ) {
584 $options['firewall'][ $key ] = array_unique( $new_list );
585 $fixed = true;
586 }
587 }
588 }
589
590 if ( $fixed ) {
591 update_option( Vigilante_Settings::OPTION_NAME, $options );
592 // Clear settings cache so changes take effect immediately
593 $this->settings->clear_cache();
594 }
595 }
596
597 /**
598 * Add admin menu page in last position
599 */
600 public function add_menu() {
601 $menu_title = __( 'Vigilant', 'vigilante' );
602
603 // Count pending approvals (separate concern, always red if present)
604 $pending_count = $this->get_pending_approvals_count();
605
606 // Get security issues with severity
607 $security_status = $this->get_security_status_for_badge();
608
609 // Total count for badge
610 $total_badge = $pending_count + $security_status['count'];
611
612 if ( $total_badge > 0 ) {
613 // Determine badge color:
614 // - Red (awaiting-mod): pending approvals OR critical modules disabled
615 // - Orange (update-plugins): only non-critical modules disabled
616 if ( $pending_count > 0 || $security_status['has_critical'] ) {
617 $badge_class = 'awaiting-mod';
618 } else {
619 $badge_class = 'update-plugins vigilante-badge-warning';
620 }
621
622 $menu_title .= sprintf(
623 ' <span class="%s count-%d"><span class="pending-count">%d</span></span>',
624 esc_attr( $badge_class ),
625 $total_badge,
626 $total_badge
627 );
628 }
629
630 add_menu_page(
631 __( 'Vigilant', 'vigilante' ),
632 $menu_title,
633 'manage_options',
634 'vigilante',
635 array( $this, 'render_settings_page' ),
636 'dashicons-shield',
637 999
638 );
639
640 // Rename auto-generated first submenu to "Dashboard"
641 add_submenu_page(
642 'vigilante',
643 __( 'Dashboard', 'vigilante' ),
644 __( 'Dashboard', 'vigilante' ),
645 'manage_options',
646 'vigilante',
647 array( $this, 'render_settings_page' )
648 );
649
650 // Security Audit shortcut
651 add_submenu_page(
652 'vigilante',
653 __( 'Security Audit', 'vigilante' ),
654 __( 'Security Audit', 'vigilante' ),
655 'manage_options',
656 'vigilante-activity-log',
657 array( $this, 'redirect_to_tab' )
658 );
659
660 // File Integrity shortcut
661 add_submenu_page(
662 'vigilante',
663 __( 'File Integrity', 'vigilante' ),
664 __( 'File Integrity', 'vigilante' ),
665 'manage_options',
666 'vigilante-file-integrity',
667 array( $this, 'redirect_to_tab' )
668 );
669 }
670
671 /**
672 * Redirect submenu shortcuts early, before headers are sent
673 */
674 public function redirect_submenu_shortcuts() {
675 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Just reading page slug for redirect
676 $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
677
678 $tab_map = array(
679 'vigilante-activity-log' => 'activity-log',
680 'vigilante-file-integrity' => 'file-integrity',
681 );
682
683 if ( isset( $tab_map[ $page ] ) ) {
684 wp_safe_redirect( admin_url( 'admin.php?page=vigilante&tab=' . $tab_map[ $page ] ) );
685 exit;
686 }
687 }
688
689 /**
690 * Fallback redirect for submenu shortcuts (JS-based)
691 */
692 public function redirect_to_tab() {
693 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Just reading page slug for redirect
694 $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
695
696 $tab_map = array(
697 'vigilante-activity-log' => 'activity-log',
698 'vigilante-file-integrity' => 'file-integrity',
699 );
700
701 if ( isset( $tab_map[ $page ] ) ) {
702 $url = admin_url( 'admin.php?page=vigilante&tab=' . $tab_map[ $page ] );
703 echo '<script>window.location.replace(' . wp_json_encode( esc_url( $url ) ) . ');</script>';
704 }
705 }
706
707 /**
708 * Highlight the correct submenu item based on active tab
709 *
710 * @param string $submenu_file Current submenu file.
711 * @return string
712 */
713 public function highlight_submenu_tab( $submenu_file ) {
714 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading tab for menu highlight only
715 $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
716 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
717 $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : '';
718
719 if ( 'vigilante' !== $page || empty( $tab ) ) {
720 return $submenu_file;
721 }
722
723 $tab_to_submenu = array(
724 'activity-log' => 'vigilante-activity-log',
725 'file-integrity' => 'vigilante-file-integrity',
726 );
727
728 if ( isset( $tab_to_submenu[ $tab ] ) ) {
729 return $tab_to_submenu[ $tab ];
730 }
731
732 return $submenu_file;
733 }
734
735 /**
736 * Set browser tab title to show plugin name and active tab
737 *
738 * Changes "Dashboard ‹ Site Name — WordPress" to
739 * "Vigilant > Dashboard ‹ Site Name — WordPress"
740 *
741 * @param string $admin_title Full admin title.
742 * @param string $title Page title from add_menu_page/add_submenu_page.
743 * @return string Modified title.
744 */
745 public function set_admin_page_title( $admin_title, $title ) {
746 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading page slug for title only
747 $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
748
749 // Only modify on Vigilante pages
750 if ( 0 !== strpos( $page, 'vigilante' ) ) {
751 return $admin_title;
752 }
753
754 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
755 $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'dashboard';
756
757 if ( isset( $this->tabs[ $tab ] ) ) {
758 $tab_label = $this->tabs[ $tab ];
759 } else {
760 $tab_label = __( 'Dashboard', 'vigilante' );
761 }
762
763 $plugin_title = __( 'Vigilant', 'vigilante' ) . ' &rsaquo; ' . $tab_label;
764
765 // Replace the original page title portion
766 return str_replace( $title, $plugin_title, $admin_title );
767 }
768
769 /**
770 * Get count of users pending approval
771 *
772 * @return int Count of pending users.
773 */
774 private function get_pending_approvals_count() {
775 // Prevent early execution before WordPress is ready
776 if ( ! did_action( 'plugins_loaded' ) ) {
777 return 0;
778 }
779
780 $registration_approval = $this->settings->get_section( 'user_security' );
781 $approval_settings = $registration_approval['registration_approval'] ?? array();
782
783 if ( empty( $approval_settings['enabled'] ) ) {
784 return 0;
785 }
786
787 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Limited results in admin context.
788 $pending_users = get_users( array(
789 'meta_key' => 'vigilante_pending_approval',
790 'meta_value' => '1',
791 'fields' => 'ID',
792 ) );
793 // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
794
795 return count( $pending_users );
796 }
797
798 /**
799 * Calculate comprehensive security score (0-100)
800 *
801 * @param array $options Plugin options.
802 * @return int Security score.
803 */
804 private function calculate_security_score( $options ) {
805 $score = 0;
806 $max_score = 0;
807
808 // Module scores (60 points total)
809 $module_weights = array(
810 'firewall' => 10,
811 'security_headers' => 8,
812 'login_security' => 10,
813 'rest_api_security' => 6,
814 'user_security' => 8,
815 'wp_hardening' => 8,
816 'file_integrity' => 5,
817 'activity_log' => 5,
818 );
819
820 foreach ( $module_weights as $module => $weight ) {
821 $max_score += $weight;
822 if ( ! empty( $options['modules'][ $module ] ) ) {
823 $score += $weight;
824 }
825 }
826
827 // Firewall details (10 points)
828 if ( ! empty( $options['modules']['firewall'] ) ) {
829 $firewall = $options['firewall'] ?? array();
830 $max_score += 10;
831
832 $firewall_checks = array(
833 'block_sql_injection',
834 'block_xss_attacks',
835 'block_bad_query_strings',
836 'block_file_inclusion',
837 'block_directory_traversal',
838 );
839
840 $firewall_enabled = 0;
841 foreach ( $firewall_checks as $check ) {
842 if ( ! empty( $firewall[ $check ] ) ) {
843 $firewall_enabled++;
844 }
845 }
846 $score += min( 10, $firewall_enabled * 2 );
847 }
848
849 // Login security details (10 points)
850 if ( ! empty( $options['modules']['login_security'] ) ) {
851 $login = $options['login_security'] ?? array();
852 $max_score += 10;
853
854 // Max attempts configured
855 if ( isset( $login['max_attempts'] ) && $login['max_attempts'] <= 5 ) {
856 $score += 3;
857 }
858 // XML-RPC disabled
859 if ( ! empty( $login['disable_xmlrpc'] ) ) {
860 $score += 3;
861 }
862 // 2FA enabled
863 if ( ! empty( $login['two_factor']['enabled'] ) ) {
864 $score += 4;
865 }
866 }
867
868 // Security headers details (10 points)
869 if ( ! empty( $options['modules']['security_headers'] ) ) {
870 $headers = $options['security_headers'] ?? array();
871 $max_score += 10;
872
873 if ( ! empty( $headers['x_frame_options'] ) ) {
874 $score += 2;
875 }
876 if ( ! empty( $headers['x_content_type_options'] ) ) {
877 $score += 2;
878 }
879 if ( ! empty( $headers['hsts']['enabled'] ) ) {
880 $score += 3;
881 }
882 if ( ! empty( $headers['csp']['enabled'] ) ) {
883 $score += 3;
884 }
885 }
886
887 // User security details (10 points)
888 if ( ! empty( $options['modules']['user_security'] ) ) {
889 $user = $options['user_security'] ?? array();
890 $max_score += 10;
891
892 if ( ! empty( $user['block_insecure_usernames'] ) ) {
893 $score += 3;
894 }
895 if ( ! empty( $user['force_strong_passwords'] ) ) {
896 $score += 3;
897 }
898 if ( ! empty( $user['password_expiration']['enabled'] ) ) {
899 $score += 2;
900 }
901 if ( ! empty( $user['email_verification']['enabled'] ) ) {
902 $score += 2;
903 }
904 }
905
906 // Audit alerts details (6 points) - only when Security Audit is on,
907 // because the alerting layer rides on top of the activity log. Leaving
908 // both alert legs off keeps these points unearned.
909 if ( ! empty( $options['modules']['activity_log'] ) ) {
910 $alerts = isset( $options['audit_alerts'] ) ? (array) $options['audit_alerts'] : array();
911 $max_score += 6;
912 if ( Vigilante_Audit_Alerts::immediate_is_active( $alerts ) ) {
913 $score += 3;
914 }
915 if ( Vigilante_Audit_Alerts::threshold_is_active( $alerts ) ) {
916 $score += 3;
917 }
918 }
919
920 // Environment checks (8 points) - penalize insecure server configuration
921 $max_score += 8;
922 $env_score = 8;
923
924 // WP_DEBUG active in production is a security risk (exposes paths, errors)
925 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
926 $env_score -= 5;
927 }
928
929 // Accounts with insecure usernames (targeted by brute force attacks)
930 $insecure_admins = $this->get_insecure_admin_usernames();
931 if ( ! empty( $insecure_admins ) ) {
932 $env_score -= 3;
933 }
934
935 $score += max( 0, $env_score );
936
937 return $max_score > 0 ? round( ( $score / $max_score ) * 100 ) : 0;
938 }
939
940 /**
941 * Get security recommendations based on current settings
942 *
943 * @param array $options Plugin options.
944 * @return array Array of recommendations.
945 */
946 private function get_security_recommendations( $options ) {
947 $recommendations = array();
948
949 // Critical: Firewall disabled
950 if ( empty( $options['modules']['firewall'] ) ) {
951 $recommendations[] = array(
952 'icon' => 'warning',
953 'priority' => 'critical',
954 'message' => __( 'Enable Firewall to protect against common attacks.', 'vigilante' ),
955 );
956 }
957
958 // Critical: Login security disabled
959 if ( empty( $options['modules']['login_security'] ) ) {
960 $recommendations[] = array(
961 'icon' => 'warning',
962 'priority' => 'critical',
963 'message' => __( 'Enable Login Security to prevent brute force attacks.', 'vigilante' ),
964 );
965 }
966
967 // High: Security headers disabled
968 if ( empty( $options['modules']['security_headers'] ) ) {
969 $recommendations[] = array(
970 'icon' => 'admin-generic',
971 'priority' => 'high',
972 'message' => __( 'Enable Security Headers to protect against clickjacking and XSS.', 'vigilante' ),
973 );
974 }
975
976 // High: User security disabled
977 if ( empty( $options['modules']['user_security'] ) ) {
978 $recommendations[] = array(
979 'icon' => 'admin-users',
980 'priority' => 'high',
981 'message' => __( 'Enable User Security to enforce password policies and username protection.', 'vigilante' ),
982 );
983 }
984
985 // High: 2FA not enabled (only if login security is active)
986 $login = $options['login_security'] ?? array();
987 if ( ! empty( $options['modules']['login_security'] ) && empty( $login['two_factor']['enabled'] ) ) {
988 $recommendations[] = array(
989 'icon' => 'shield',
990 'priority' => 'high',
991 'message' => __( 'Enable Two-Factor Authentication for enhanced login security.', 'vigilante' ),
992 'tab' => 'login',
993 );
994 }
995
996 // Medium: REST API security disabled
997 if ( empty( $options['modules']['rest_api_security'] ) ) {
998 $recommendations[] = array(
999 'icon' => 'rest-api',
1000 'priority' => 'medium',
1001 'message' => __( 'Enable REST API Security to control API access and prevent enumeration.', 'vigilante' ),
1002 );
1003 }
1004
1005 // Medium: WP Hardening disabled
1006 if ( empty( $options['modules']['wp_hardening'] ) ) {
1007 $recommendations[] = array(
1008 'icon' => 'lock',
1009 'priority' => 'medium',
1010 'message' => __( 'Enable WP Hardening to remove version info and protect core files.', 'vigilante' ),
1011 );
1012 }
1013
1014 // Medium: File integrity disabled
1015 if ( empty( $options['modules']['file_integrity'] ) ) {
1016 $recommendations[] = array(
1017 'icon' => 'media-text',
1018 'priority' => 'medium',
1019 'message' => __( 'Enable File Integrity to detect unauthorized file changes.', 'vigilante' ),
1020 );
1021 }
1022
1023 // Medium: Security Audit disabled
1024 if ( empty( $options['modules']['activity_log'] ) ) {
1025 $recommendations[] = array(
1026 'icon' => 'list-view',
1027 'priority' => 'medium',
1028 'message' => __( 'Enable Security Audit to track security events.', 'vigilante' ),
1029 );
1030 }
1031
1032 // Low: XML-RPC enabled (only if login security is active)
1033 if ( ! empty( $options['modules']['login_security'] ) && empty( $login['disable_xmlrpc'] ) ) {
1034 $recommendations[] = array(
1035 'icon' => 'info',
1036 'priority' => 'low',
1037 'message' => __( 'Disable XML-RPC if not needed (reduces attack surface).', 'vigilante' ),
1038 'tab' => 'login',
1039 );
1040 }
1041
1042 // Low: Strong passwords not enforced (only if user security is active)
1043 $user = $options['user_security'] ?? array();
1044 if ( ! empty( $options['modules']['user_security'] ) && empty( $user['force_strong_passwords'] ) ) {
1045 $recommendations[] = array(
1046 'icon' => 'admin-users',
1047 'priority' => 'low',
1048 'message' => __( 'Enforce strong passwords for all users.', 'vigilante' ),
1049 'tab' => 'users',
1050 );
1051 }
1052
1053 // High: WP_DEBUG active in production (regardless of Vigilante settings)
1054 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
1055 $recommendations[] = array(
1056 'icon' => 'warning',
1057 'priority' => 'high',
1058 'message' => __( 'WP_DEBUG is active. Debug mode exposes sensitive information and should be disabled in production.', 'vigilante' ),
1059 'tab' => 'wp-hardening',
1060 );
1061 }
1062
1063 // Low: Users with display name matching login username (only if user security active)
1064 if ( ! empty( $options['modules']['user_security'] ) ) {
1065 $exposed_users = $this->get_users_with_exposed_login();
1066 if ( ! empty( $exposed_users ) ) {
1067 $recommendations[] = array(
1068 'icon' => 'admin-users',
1069 'priority' => 'low',
1070 'message' => sprintf(
1071 /* translators: %s: Comma-separated list of usernames */
1072 __( 'These users have their login username as display name (publicly visible): %s', 'vigilante' ),
1073 implode( ', ', $exposed_users )
1074 ),
1075 'tab' => 'users',
1076 );
1077 }
1078 }
1079
1080 // High: Accounts with insecure usernames (regardless of module status)
1081 $insecure_admins = $this->get_insecure_admin_usernames();
1082 if ( ! empty( $insecure_admins ) ) {
1083 $recommendations[] = array(
1084 'icon' => 'warning',
1085 'priority' => 'high',
1086 'message' => sprintf(
1087 /* translators: %s: Comma-separated list of usernames */
1088 __( 'Insecure usernames detected: %s. These are commonly targeted in brute force attacks. Create new accounts with unique usernames and remove these.', 'vigilante' ),
1089 implode( ', ', $insecure_admins )
1090 ),
1091 );
1092 }
1093
1094 // Critical: Closed or removed plugins detected by the daily check.
1095 // Reads from the cached state map populated by Vigilante_Plugin_Status, so
1096 // there is no extra HTTP call here. Ignored slugs are filtered out so the
1097 // recommendation respects the user's per-slug Ignore decisions.
1098 if ( ! empty( $options['modules']['file_integrity'] ) && ! empty( $options['file_integrity']['check_closed_plugins'] ) ) {
1099 if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) {
1100 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
1101 }
1102 $closed_checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
1103 $closed_active = $closed_checker->get_closed_plugins();
1104 if ( ! empty( $closed_active ) ) {
1105 $names = array();
1106 foreach ( $closed_active as $slug => $entry ) {
1107 $names[] = isset( $entry['name'] ) ? $entry['name'] : $slug;
1108 }
1109 $recommendations[] = array(
1110 'icon' => 'warning',
1111 'priority' => 'critical',
1112 'message' => sprintf(
1113 /* translators: 1: count, 2: comma-separated plugin names */
1114 _n(
1115 '%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.',
1116 '%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.',
1117 count( $closed_active ),
1118 'vigilante'
1119 ),
1120 count( $closed_active ),
1121 implode( ', ', $names )
1122 ),
1123 'tab' => 'file-integrity',
1124 );
1125 }
1126 }
1127
1128 // Medium: Security Audit is on but no audit alert is configured. The
1129 // alerting layer only makes sense while the activity log is running.
1130 if ( ! empty( $options['modules']['activity_log'] ) ) {
1131 $alerts = isset( $options['audit_alerts'] ) ? (array) $options['audit_alerts'] : array();
1132 if ( ! Vigilante_Audit_Alerts::has_active_alerts( $alerts ) ) {
1133 $recommendations[] = array(
1134 'icon' => 'email-alt',
1135 'priority' => 'medium',
1136 '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' ),
1137 'tab' => 'activity-log',
1138 );
1139 }
1140 }
1141
1142 // Sort by priority
1143 $priority_order = array( 'critical' => 0, 'high' => 1, 'medium' => 2, 'low' => 3 );
1144 usort( $recommendations, function( $a, $b ) use ( $priority_order ) {
1145 return ( $priority_order[ $a['priority'] ] ?? 99 ) - ( $priority_order[ $b['priority'] ] ?? 99 );
1146 } );
1147
1148 return $recommendations;
1149 }
1150
1151 /**
1152 * Get users whose display name matches their login username
1153 *
1154 * Limited to administrators and editors for performance and relevance.
1155 * Cached with transient to avoid repeated queries on every dashboard load.
1156 *
1157 * @return array Array of usernames with exposed login.
1158 */
1159 private function get_users_with_exposed_login() {
1160 $cache_key = 'vigilante_exposed_display_names';
1161 $cached = get_transient( $cache_key );
1162
1163 if ( false !== $cached ) {
1164 return $cached;
1165 }
1166
1167 $exposed = array();
1168 $users = get_users( array(
1169 'role__in' => array( 'administrator', 'editor' ),
1170 'fields' => array( 'ID', 'user_login', 'display_name' ),
1171 ) );
1172
1173 foreach ( $users as $user ) {
1174 if ( strcasecmp( $user->display_name, $user->user_login ) === 0 ) {
1175 $exposed[] = $user->user_login;
1176 }
1177 }
1178
1179 // Cache for 12 hours
1180 set_transient( $cache_key, $exposed, 12 * HOUR_IN_SECONDS );
1181
1182 return $exposed;
1183 }
1184
1185 /**
1186 * Get accounts with insecure usernames
1187 *
1188 * Checks for common default usernames that are targeted by brute force attacks.
1189 * Detects any user regardless of role (consistent with username creation blocking).
1190 * Uses WordPress object cache via get_user_by() so no transient needed.
1191 *
1192 * @return array Array of insecure usernames found.
1193 */
1194 private function get_insecure_admin_usernames() {
1195 $priority_usernames = array( 'admin', 'administrator', 'root', 'test', 'user', 'guest', 'info', 'sysadmin', 'webmaster' );
1196 $found = array();
1197
1198 foreach ( $priority_usernames as $username ) {
1199 $user = get_user_by( 'login', $username );
1200 if ( $user ) {
1201 $found[] = $username;
1202 }
1203 }
1204
1205 return $found;
1206 }
1207
1208 /**
1209 * Get security status for menu badge
1210 *
1211 * Returns count of disabled modules and whether there are critical issues.
1212 * Critical = Firewall or Login Security disabled.
1213 *
1214 * @return array Array with 'count' and 'has_critical'.
1215 */
1216 public function get_security_status_for_badge() {
1217 $options = $this->settings->get_all_options();
1218 $modules = $options['modules'] ?? array();
1219
1220 // Count disabled modules
1221 $disabled_count = 0;
1222 $has_critical = false;
1223
1224 // Critical modules - if disabled, badge is red
1225 $critical_modules = array( 'firewall', 'login_security' );
1226
1227 foreach ( $modules as $module => $enabled ) {
1228 // Handle both boolean and string values ('1', '0', true, false)
1229 $is_enabled = filter_var( $enabled, FILTER_VALIDATE_BOOLEAN );
1230
1231 if ( ! $is_enabled ) {
1232 $disabled_count++;
1233
1234 // Check if this is a critical module
1235 if ( in_array( $module, $critical_modules, true ) ) {
1236 $has_critical = true;
1237 }
1238 }
1239 }
1240
1241 return array(
1242 'count' => $disabled_count,
1243 'has_critical' => $has_critical,
1244 );
1245 }
1246
1247 /**
1248 * Get count of security issues for menu badge (deprecated, use get_security_status_for_badge)
1249 *
1250 * @return int Count of critical/high issues.
1251 */
1252 public function get_security_issues_count() {
1253 $status = $this->get_security_status_for_badge();
1254 return $status['count'];
1255 }
1256
1257 /**
1258 * Register settings
1259 */
1260 public function register_settings() {
1261 register_setting(
1262 'vigilante_options',
1263 Vigilante_Settings::OPTION_NAME,
1264 array( $this->settings, 'validate_options' )
1265 );
1266 }
1267
1268 /**
1269 * Index behind the settings search box.
1270 *
1271 * Each entry points at one settings row. The search matches on the label,
1272 * on its English original and on 'keywords', which are extra terms someone
1273 * might type instead of the label itself.
1274 *
1275 * Those keywords are wrapped in _x() with the context "settings search
1276 * keywords" so every locale can supply its own: the source strings are in
1277 * English, and a Spanish user typing "contrasena" or a German one typing
1278 * "Kennwort" only reaches the password settings if that locale translated
1279 * them. Translators can add, drop or replace terms freely, one per space;
1280 * they are never displayed, only matched against what the user types.
1281 *
1282 * The list is maintained by hand, so a new settings row needs an entry here
1283 * or it cannot be found. It had drifted to 68 of 131 rows before 2.9.7.
1284 *
1285 * @return array
1286 */
1287 private function get_search_index() {
1288 return array(
1289 // Firewall - Main
1290 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' ) ),
1291 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' ) ),
1292 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' ) ),
1293 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' ) ),
1294 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' ) ),
1295 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' ) ),
1296 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' ) ),
1297 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' ) ),
1298 // Firewall - Server Protection
1299 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' ) ),
1300 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' ) ),
1301 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' ) ),
1302 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' ) ),
1303 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' ) ),
1304 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' ) ),
1305 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' ) ),
1306 // Security Headers
1307 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' ) ),
1308 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' ) ),
1309 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' ) ),
1310 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' ) ),
1311 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' ) ),
1312 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' ) ),
1313 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' ) ),
1314 // Security Headers - Cross-Origin Policies
1315 array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Cross-Origin Policies', 'vigilante' ), 'anchor' => 'vigilante-section-headers-cross-origin', 'label' => __( 'Cross-Origin-Opener-Policy (COOP)', 'vigilante' ), 'label_en' => 'Cross-Origin-Opener-Policy (COOP)', 'keywords' => _x( 'coop cross-origin opener policy popup popups window opener tag assistant google isolation browsing context headers', 'settings search keywords', 'vigilante' ) ),
1316 array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Cross-Origin Policies', 'vigilante' ), 'anchor' => 'vigilante-section-headers-cross-origin', 'label' => __( 'Cross-Origin-Embedder-Policy (COEP)', 'vigilante' ), 'label_en' => 'Cross-Origin-Embedder-Policy (COEP)', 'keywords' => _x( 'coep cross-origin embedder policy require-corp credentialless embed embeds iframe fonts headers', 'settings search keywords', 'vigilante' ) ),
1317 array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Cross-Origin Policies', 'vigilante' ), 'anchor' => 'vigilante-section-headers-cross-origin', 'label' => __( 'Cross-Origin-Resource-Policy (CORP)', 'vigilante' ), 'label_en' => 'Cross-Origin-Resource-Policy (CORP)', 'keywords' => _x( 'corp cross-origin resource policy hotlink hotlinking cdn images assets headers', 'settings search keywords', 'vigilante' ) ),
1318 // Login Security
1319 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' ) ),
1320 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' ) ),
1321 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' ) ),
1322 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' ) ),
1323 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' ) ),
1324 // REST API
1325 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' ) ),
1326 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' ) ),
1327 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' ) ),
1328 // User Security
1329 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' ) ),
1330 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' ) ),
1331 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' ) ),
1332 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' ) ),
1333 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' ) ),
1334 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' ) ),
1335 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' ) ),
1336 // WP Hardening
1337 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' ) ),
1338 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' ) ),
1339 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' ) ),
1340 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' ) ),
1341 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' ) ),
1342 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' ) ),
1343 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' ) ),
1344 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' ) ),
1345 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' ) ),
1346 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' ) ),
1347 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' ) ),
1348 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' ) ),
1349 // File Integrity
1350 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' ) ),
1351 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' ) ),
1352 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' ) ),
1353 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' ) ),
1354 // Security Audit
1355 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' ) ),
1356 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' ) ),
1357 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' ) ),
1358 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' ) ),
1359 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' ) ),
1360 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' ) ),
1361 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' ) ),
1362 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' ) ),
1363 // Settings & Tools
1364 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' ) ),
1365 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' ) ),
1366 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' ) ),
1367 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' ) ),
1368 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' ) ),
1369 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' ) ),
1370 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' ) ),
1371 // Entradas anadidas en la 2.9.7 tras comprobar que el indice cubria 68 de
1372 // las 131 filas de ajustes: buscar XML-RPC, por ejemplo, no devolvia nada.
1373 // El indice se mantiene a mano, asi que al anadir una fila de ajustes hay
1374 // que anadirla tambien aqui.
1375 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' ) ),
1376 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' ) ),
1377 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' ) ),
1378 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' ) ),
1379 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' ) ),
1380 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' ) ),
1381 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' ) ),
1382 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' ) ),
1383 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' ) ),
1384 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' ) ),
1385 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' ) ),
1386 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' ) ),
1387 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' ) ),
1388 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' ) ),
1389 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' ) ),
1390 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' ) ),
1391 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' ) ),
1392 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' ) ),
1393 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' ) ),
1394 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' ) ),
1395 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' ) ),
1396 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' ) ),
1397 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' ) ),
1398 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' ) ),
1399 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' ) ),
1400 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' ) ),
1401 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' ) ),
1402 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' ) ),
1403 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' ) ),
1404 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' ) ),
1405 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' ) ),
1406 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' ) ),
1407 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' ) ),
1408 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' ) ),
1409 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' ) ),
1410 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' ) ),
1411 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' ) ),
1412 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' ) ),
1413 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' ) ),
1414 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' ) ),
1415 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' ) ),
1416 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' ) ),
1417 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' ) ),
1418 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' ) ),
1419 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' ) ),
1420 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' ) ),
1421 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' ) ),
1422 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' ) ),
1423 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' ) ),
1424 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' ) ),
1425 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' ) ),
1426 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' ) ),
1427 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' ) ),
1428 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' ) ),
1429 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' ) ),
1430 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' ) ),
1431 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' ) ),
1432 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' ) ),
1433 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' ) ),
1434 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' ) ),
1435 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' ) ),
1436 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' ) ),
1437 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' ) ),
1438 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' ) ),
1439 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' ) ),
1440 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' ) ),
1441 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' ) ),
1442 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' ) ),
1443 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' ) ),
1444 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' ) ),
1445 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' ) ),
1446 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' ) ),
1447 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' ) ),
1448 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' ) ),
1449 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' ) ),
1450 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' ) ),
1451 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' ) ),
1452 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' ) ),
1453 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' ) ),
1454 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' ) ),
1455 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' ) ),
1456 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' ) ),
1457 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' ) ),
1458 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' ) ),
1459 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' ) ),
1460 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' ) ),
1461 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' ) ),
1462 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' ) ),
1463 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' ) ),
1464 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' ) ),
1465 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' ) ),
1466 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' ) ),
1467 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' ) ),
1468 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' ) ),
1469 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' ) ),
1470 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' ) ),
1471 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' ) ),
1472 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' ) ),
1473 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' ) ),
1474 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' ) ),
1475 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' ) ),
1476 );
1477 }
1478
1479 /**
1480 * Enqueue admin assets
1481 *
1482 * @param string $hook Current admin page.
1483 */
1484 public function enqueue_assets( $hook ) {
1485 // toplevel_page_vigilante for top-level menu page
1486 if ( 'toplevel_page_vigilante' !== $hook ) {
1487 return;
1488 }
1489
1490 wp_enqueue_style(
1491 'vigilante-admin',
1492 VIGILANTE_ASSETS_URL . 'css/admin.css',
1493 array(),
1494 VIGILANTE_VERSION
1495 );
1496
1497 wp_enqueue_script(
1498 'vigilante-admin',
1499 VIGILANTE_ASSETS_URL . 'js/admin.js',
1500 array( 'jquery' ),
1501 VIGILANTE_VERSION,
1502 true
1503 );
1504
1505 wp_localize_script( 'vigilante-admin', 'vigilanteAdmin', array(
1506 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
1507 'nonce' => wp_create_nonce( 'vigilante_admin_nonce' ),
1508 'currentUserId' => get_current_user_id(),
1509 'logoutUrl' => wp_logout_url( wp_login_url() ),
1510 'adminUrl' => admin_url( 'admin.php?page=vigilante' ),
1511 'searchIndex' => $this->get_search_index(),
1512 // The scan repaints this table from JavaScript, so the same gate
1513 // has to travel with it or half the screen keeps the dead button.
1514 'approvalLocked' => $this->critical_approval_locked(),
1515 'underAttack' => array(
1516 'active' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->is_active(),
1517 'remaining' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->get_remaining_time(),
1518 ),
1519 'strings' => array(
1520 'saving' => __( 'Saving...', 'vigilante' ),
1521 'sendingTest' => __( 'Sending...', 'vigilante' ),
1522 'saved' => __( 'Settings saved', 'vigilante' ),
1523 'error' => __( 'Error saving settings', 'vigilante' ),
1524 'confirm' => __( 'Are you sure?', 'vigilante' ),
1525 'scanning' => __( 'Scanning...', 'vigilante' ),
1526 'scanComplete' => __( 'Scan complete', 'vigilante' ),
1527 'loading' => __( 'Loading...', 'vigilante' ),
1528 'searching' => __( 'Searching...', 'vigilante' ),
1529 'noUsersFound' => __( 'No users found', 'vigilante' ),
1530 'searchError' => __( 'Error searching users', 'vigilante' ),
1531 'sending' => __( 'Sending...', 'vigilante' ),
1532 'sendNotification' => __( 'Send notification now', 'vigilante' ),
1533 'notificationsSent' => __( 'notifications sent', 'vigilante' ),
1534 'skipped' => __( 'skipped', 'vigilante' ),
1535 'failed' => __( 'failed', 'vigilante' ),
1536 'customConfig' => __( 'Custom Configuration', 'vigilante' ),
1537 // Header tester strings
1538 'testHeaders' => __( 'Test Headers', 'vigilante' ),
1539 'testing' => __( 'Testing...', 'vigilante' ),
1540 'score' => __( 'Score', 'vigilante' ),
1541 'enabledHeaders' => __( 'Enabled headers', 'vigilante' ),
1542 'missingHeaders' => __( 'Missing headers', 'vigilante' ),
1543 'warnings' => __( 'Warnings', 'vigilante' ),
1544 // File integrity scan results strings
1545 'scanResults' => __( 'Scan Results', 'vigilante' ),
1546 'ok' => __( 'OK', 'vigilante' ),
1547 'modified' => __( 'Modified', 'vigilante' ),
1548 'suspicious' => __( 'Suspicious', 'vigilante' ),
1549 'totalScanned' => __( 'Total Scanned', 'vigilante' ),
1550 'suspiciousFiles' => __( 'Suspicious Files', 'vigilante' ),
1551 'suspiciousWarning' => __( 'These files may contain malicious code or are in unexpected locations. Review immediately!', 'vigilante' ),
1552 'file' => __( 'File', 'vigilante' ),
1553 'reason' => __( 'Reason', 'vigilante' ),
1554 'type' => __( 'Type', 'vigilante' ),
1555 'unknown' => __( 'Unknown', 'vigilante' ),
1556 'modifiedFiles' => __( 'Modified Files', 'vigilante' ),
1557 'modifiedDescription' => __( 'These files (apparently) differ from the original WordPress or plugin versions.', 'vigilante' ),
1558 'extraFiles' => __( 'Extra Files', 'vigilante' ),
1559 'extra' => __( 'Extra', 'vigilante' ),
1560 'ignored' => __( 'Ignored', 'vigilante' ),
1561 'extraDescription' => __( 'PHP files found in plugins or themes that are not part of the original distribution from WordPress.org.', 'vigilante' ),
1562 'actions' => __( 'Actions', 'vigilante' ),
1563 'ignore' => __( 'Ignore', 'vigilante' ),
1564 'ignoring' => __( 'Ignoring...', 'vigilante' ),
1565 'fileIgnored' => __( 'File added to ignored list.', 'vigilante' ),
1566 'fileUnignored' => __( 'File removed from ignored list.', 'vigilante' ),
1567 'confirmClearIgnored' => __( 'Remove all files from the ignored list? They will appear in scan results again.', 'vigilante' ),
1568 'ignoredCleared' => __( 'Ignored files list cleared. Page will reload...', 'vigilante' ),
1569 'selectAll' => __( 'Select all', 'vigilante' ),
1570 'bulkIgnoreSelected' => __( 'Ignore selected', 'vigilante' ),
1571 'bulkUnignoreSelected'=> __( 'Stop ignoring selected', 'vigilante' ),
1572 'bulkNoSelection' => __( 'Select at least one file first.', 'vigilante' ),
1573 'bulkConfirmIgnore' => __( 'Ignore the selected files? They will be hidden from future scan results until you remove them from the ignored list.', 'vigilante' ),
1574 'bulkConfirmUnignore' => __( 'Remove the selected files from the ignored list? They will appear in scan results again.', 'vigilante' ),
1575 'bulkProcessing' => __( 'Processing...', 'vigilante' ),
1576 /* translators: %d: number of files selected for bulk action. */
1577 'bulkSelectedCount' => __( '%d selected', 'vigilante' ),
1578 'allClear' => __( 'All files verified - no issues found!', 'vigilante' ),
1579 'criticalConfigTitle' => __( 'Critical config files modified', 'vigilante' ),
1580 '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' ),
1581 'approve' => __( 'Approve', 'vigilante' ),
1582 'approving' => __( 'Approving...', 'vigilante' ),
1583 'approvalLockedNotice' => $this->critical_approval_notice(),
1584 'criticalApproved' => __( 'Change approved. Next scan will use the current state as baseline.', 'vigilante' ),
1585 'reviewChanges' => __( 'Review changes', 'vigilante' ),
1586 'hideChanges' => __( 'Hide changes', 'vigilante' ),
1587 'changes' => __( 'Changes', 'vigilante' ),
1588 'diffUnavailable' => __( 'Diff not available for this file (baseline was created before diff tracking was added). Approve to enable diff on future changes.', 'vigilante' ),
1589 'diffNetwork' => __( 'This file belongs to the whole network, so its line changes are only shown to network administrators, on the main site.', 'vigilante' ),
1590 'diffRescan' => __( 'Run a new scan to see the line changes of this file.', 'vigilante' ),
1591 'diffRedaction' => __( 'The line changes of this file are not shown because a value in it could not be hidden safely. The change itself is still detected.', 'vigilante' ),
1592 'diffEmpty' => __( 'No line-level changes detected (may be whitespace or reordering).', 'vigilante' ),
1593 'diffLines' => __( 'lines', 'vigilante' ),
1594 // Under Attack mode strings
1595 'underAttackConfirmActivate' => __( 'Activate Under Attack mode? All visitors will see a verification page for the next 4 hours.', 'vigilante' ),
1596 'underAttackConfirmDeactivate' => __( 'Deactivate Under Attack mode?', 'vigilante' ),
1597 'underAttackActivating' => __( 'Activating...', 'vigilante' ),
1598 'underAttackDeactivating' => __( 'Deactivating...', 'vigilante' ),
1599 // Database backup strings
1600 'dbBackupDownloading' => __( 'Generating backup...', 'vigilante' ),
1601 'dbBackupNoTables' => __( 'Please select at least one table.', 'vigilante' ),
1602 'dbBackupSuccess' => __( 'Database backup downloaded successfully.', 'vigilante' ),
1603 // Firewall unblock
1604 'confirmUnblockIp' => __( 'Unblock this IP from firewall rate limiting?', 'vigilante' ),
1605 // Database prefix strings
1606 'dbPrefixConfirm' => __( 'This operation will change your database prefix. It is irreversible. Make sure you have a current database backup before proceeding.', 'vigilante' ),
1607 'dbPrefixChanging' => __( 'Changing prefix...', 'vigilante' ),
1608 'dbPrefixSuccess' => __( 'Database prefix changed successfully. The page will reload now.', 'vigilante' ),
1609 'dbPrefixCheckbox' => __( 'You must confirm that you have a database backup.', 'vigilante' ),
1610 /* translators: 1: Hours, 2: Minutes */
1611 'underAttackRemaining' => __( '%1$dh %2$dm remaining', 'vigilante' ),
1612 'underAttackLabel' => __( 'Under Attack', 'vigilante' ),
1613 'standardLabel' => __( 'Standard', 'vigilante' ),
1614 'maximumLabel' => __( 'Maximum Security', 'vigilante' ),
1615 'deactivate' => __( 'Deactivate', 'vigilante' ),
1616 'underAttackActivate' => __( 'Activate for 4 hours', 'vigilante' ),
1617 // Settings strings
1618 'saveSettings' => __( 'Save Settings', 'vigilante' ),
1619 'settingsResetDefaults' => __( 'Settings reset to defaults.', 'vigilante' ),
1620 'confirmOverwrite' => __( 'This will overwrite your current settings.', 'vigilante' ),
1621 'importFailed' => __( 'Could not import settings. Check the file and try again.', 'vigilante' ),
1622 /* translators: 1: tests passed, 2: total tests in this category */
1623 'testsCounter' => __( '%1$d/%2$d tests', 'vigilante' ),
1624 'confirmResetAll' => __( 'This will reset ALL settings to defaults.', 'vigilante' ),
1625 'couldNotDetermineSection' => __( 'Could not determine section.', 'vigilante' ),
1626 'confirmResetSection' => __( 'Reset this section to default values? This cannot be undone.', 'vigilante' ),
1627 'sectionResetDefaults' => __( 'Section reset to defaults.', 'vigilante' ),
1628 /* translators: %s: preset name */
1629 'confirmApplyPreset' => __( 'Apply the "%s" preset?', 'vigilante' ),
1630 // Scan strings
1631 'scanFailed' => __( 'Scan failed', 'vigilante' ),
1632 /* translators: %s: error message */
1633 'scanError' => __( 'Scan error: %s', 'vigilante' ),
1634 'runScanNow' => __( 'Run Scan Now', 'vigilante' ),
1635 'confirmClearScan' => __( 'Are you sure you want to clear all scan results?', 'vigilante' ),
1636 'clearing' => __( 'Clearing...', 'vigilante' ),
1637 'scanResultsCleared' => __( 'Scan results cleared. Page will reload...', 'vigilante' ),
1638 'failedClearResults' => __( 'Failed to clear results', 'vigilante' ),
1639 /* translators: %s: error message */
1640 'ajaxError' => __( 'AJAX Error: %s', 'vigilante' ),
1641 // Activity log popup strings
1642 'logRequest' => __( 'Request', 'vigilante' ),
1643 'logDate' => __( 'Date', 'vigilante' ),
1644 'logMethod' => __( 'Method', 'vigilante' ),
1645 'logType' => __( 'Type', 'vigilante' ),
1646 'logAction' => __( 'Action', 'vigilante' ),
1647 'logSeverity' => __( 'Severity', 'vigilante' ),
1648 'logMessage' => __( 'Message', 'vigilante' ),
1649 'logRequestUri' => __( 'Address', 'vigilante' ),
1650 'logClient' => __( 'Client', 'vigilante' ),
1651 'logUser' => __( 'User', 'vigilante' ),
1652 'logIpAddress' => __( 'IP Address', 'vigilante' ),
1653 'logUserAgent' => __( 'User Agent', 'vigilante' ),
1654 'logIpLabel' => __( 'IP:', 'vigilante' ),
1655 'logUaLabel' => __( 'UA:', 'vigilante' ),
1656 'logWhitelist' => __( 'Whitelist', 'vigilante' ),
1657 'logBlacklist' => __( 'Blacklist', 'vigilante' ),
1658 'logInWhitelist' => __( 'In whitelist', 'vigilante' ),
1659 'logInBlacklist' => __( 'In blacklist', 'vigilante' ),
1660 'logAdded' => __( 'Added!', 'vigilante' ),
1661 'logErrorAddingToList' => __( 'Error adding to list', 'vigilante' ),
1662 'logRequestFailed' => __( 'Request failed', 'vigilante' ),
1663 // Activity log table strings
1664 'noLogEntries' => __( 'No log entries found.', 'vigilante' ),
1665 'view' => __( 'View', 'vigilante' ),
1666 'confirmClearLogs' => __( 'This will delete all audit logs.', 'vigilante' ),
1667 // Export logs strings
1668 'exporting' => __( 'Exporting...', 'vigilante' ),
1669 /* translators: %d: number of entries */
1670 'logsExported' => __( 'Logs exported (%d entries)', 'vigilante' ),
1671 'noLogsToExport' => __( 'No logs to export', 'vigilante' ),
1672 'exportFailed' => __( 'Export failed', 'vigilante' ),
1673 'exportLogs' => __( 'Export Logs', 'vigilante' ),
1674 // Backup strings
1675 'backupCreated' => __( 'Backup created successfully.', 'vigilante' ),
1676 'createBackupNow' => __( 'Download Backup', 'vigilante' ),
1677 'downloadBackup' => __( 'Download Backup (.zip)', 'vigilante' ),
1678 /* translators: %d: number of tables */
1679 'tablesCount' => __( '%d tables', 'vigilante' ),
1680 /* translators: 1: table count, 2: human-readable size */
1681 'dbTablesTotal' => __( '%1$d tables total (%2$s)', 'vigilante' ),
1682 /* translators: 1: selected count, 2: human-readable size */
1683 'dbTablesSelected' => __( '%1$d tables selected (%2$s)', 'vigilante' ),
1684 // Settings search strings
1685 'searchNoResults' => __( 'No matching settings found.', 'vigilante' ),
1686 /* translators: %d: number of results that did not fit in the list. */
1687 'searchMoreResults' => __( '%d more results. Refine the search to see them.', 'vigilante' ),
1688 'searchInTab' => __( 'in', 'vigilante' ),
1689 // Modules string
1690 /* translators: 1: enabled count, 2: total count */
1691 'modulesEnabled' => __( '%1$d / %2$d modules enabled', 'vigilante' ),
1692 // Activity log label maps for JS rendering
1693 'eventTypeLabels' => array(
1694 'login' => __( 'Login', 'vigilante' ),
1695 'user' => __( 'User', 'vigilante' ),
1696 'content' => __( 'Content', 'vigilante' ),
1697 'plugin' => __( 'Plugin', 'vigilante' ),
1698 'theme' => __( 'Theme', 'vigilante' ),
1699 'settings' => __( 'Settings', 'vigilante' ),
1700 'comment' => __( 'Comment', 'vigilante' ),
1701 'media' => __( 'Media', 'vigilante' ),
1702 'firewall' => __( 'Firewall', 'vigilante' ),
1703 'file' => __( 'File', 'vigilante' ),
1704 'security' => __( 'Security', 'vigilante' ),
1705 'system' => __( 'System', 'vigilante' ),
1706 ),
1707 'severityLabels' => array(
1708 'info' => __( 'Info', 'vigilante' ),
1709 'warning' => __( 'Warning', 'vigilante' ),
1710 'critical' => __( 'Critical', 'vigilante' ),
1711 ),
1712 // Password reset strings
1713 'noUsersFoundSearch' => __( 'No users found', 'vigilante' ),
1714 /* translators: %d: number of users */
1715 'confirmForceReset' => __( 'Force password reset for %d user(s)? A password reset email will be sent to each user.', 'vigilante' ),
1716 'warningResettingSelf' => __( 'WARNING: You are including yourself. Your session will end and you will need to set a new password.', 'vigilante' ),
1717 'processing' => __( 'Processing...', 'vigilante' ),
1718 'anErrorOccurred' => __( 'An error occurred', 'vigilante' ),
1719 'forceResetSelected' => __( 'Force Reset for Selected Users', 'vigilante' ),
1720 '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' ),
1721 'warningResettingSelfAll' => __( 'WARNING: You are including yourself. Your session will end immediately.', 'vigilante' ),
1722 'forceResetAll' => __( 'Force Reset for ALL Users', 'vigilante' ),
1723 // Role-based password reset strings
1724 /* translators: %d: number of users */
1725 'confirmForceResetByRole' => __( 'Force password reset for %d user(s) with the selected roles? A password reset email will be sent to each user.', 'vigilante' ),
1726 'noRolesSelected' => __( 'Please select at least one role.', 'vigilante' ),
1727 'forceResetByRole' => __( 'Force Reset for Selected Roles', 'vigilante' ),
1728 // User approval strings
1729 'confirmApprove' => __( 'Approve this user?', 'vigilante' ),
1730 'approve' => __( 'Approve', 'vigilante' ),
1731 'rejectReason' => __( 'Enter rejection reason (optional):', 'vigilante' ),
1732 'reject' => __( 'Reject', 'vigilante' ),
1733 'noPending' => __( 'No pending registrations.', 'vigilante' ),
1734 // Session management strings
1735 'confirmRevoke' => __( 'Revoke this session?', 'vigilante' ),
1736 'confirmRevokeAll' => __( 'Revoke all other sessions?', 'vigilante' ),
1737 'revokeOthers' => __( 'Revoke All Other Sessions', 'vigilante' ),
1738 'confirmRevokeAllUser' => __( 'Revoke ALL sessions for this user? They will be logged out everywhere.', 'vigilante' ),
1739 'sessionsFor' => __( 'Sessions for:', 'vigilante' ),
1740 'noSessions' => __( 'No active sessions', 'vigilante' ),
1741 'revoke' => __( 'Revoke', 'vigilante' ),
1742 'noUsers' => __( 'No users found', 'vigilante' ),
1743 // Time ago strings
1744 'timeYear' => __( 'year', 'vigilante' ),
1745 'timeYears' => __( 'years', 'vigilante' ),
1746 'timeMonth' => __( 'month', 'vigilante' ),
1747 'timeMonths' => __( 'months', 'vigilante' ),
1748 'timeDay' => __( 'day', 'vigilante' ),
1749 'timeDays' => __( 'days', 'vigilante' ),
1750 'timeHour' => __( 'hour', 'vigilante' ),
1751 'timeHours' => __( 'hours', 'vigilante' ),
1752 'timeMinute' => __( 'minute', 'vigilante' ),
1753 'timeMinutes' => __( 'minutes', 'vigilante' ),
1754 /* translators: %1$d: count, %2$s: time unit */
1755 'timeAgo' => __( '%1$d %2$s ago', 'vigilante' ),
1756 'justNow' => __( 'Just now', 'vigilante' ),
1757 // Pagination strings
1758 /* translators: 1: first item number, 2: last item number, 3: total items */
1759 'paginationOf' => __( '%1$d–%2$d of %3$d', 'vigilante' ),
1760 'paginationEmpty' => __( '0 items', 'vigilante' ),
1761 // Security Analyzer strings
1762 'analyzerScanNow' => __( 'Scan now', 'vigilante' ),
1763 'analyzerScanning' => __( 'Scanning…', 'vigilante' ),
1764 'analyzerFastPhase' => __( 'Running fast checks…', 'vigilante' ),
1765 'analyzerSlowPhase' => __( 'Running remote checks…', 'vigilante' ),
1766 'analyzerScanComplete' => __( 'Security scan complete.', 'vigilante' ),
1767 'analyzerScanFailed' => __( 'Security scan failed.', 'vigilante' ),
1768 'analyzerShowDetails' => __( 'Show detailed breakdown', 'vigilante' ),
1769 'analyzerHideDetails' => __( 'Hide detailed breakdown', 'vigilante' ),
1770 'analyzerGoToSetting' => __( 'Go to setting', 'vigilante' ),
1771 'analyzerNoData' => __( 'No data yet — run a scan to populate this category.', 'vigilante' ),
1772 'analyzerJustNow' => __( 'just now', 'vigilante' ),
1773 'analyzerAgo' => __( 'ago', 'vigilante' ),
1774 'analyzerSettingsSaved' => __( 'Analyzer settings saved.', 'vigilante' ),
1775 'analyzerLastScanJustNow' => __( 'Last scan just now', 'vigilante' ),
1776 'analyzerQualityExcellent' => __( 'Excellent', 'vigilante' ),
1777 'analyzerQualityGood' => __( 'Good', 'vigilante' ),
1778 'analyzerQualityFair' => __( 'Fair', 'vigilante' ),
1779 'analyzerQualityPoor' => __( 'Poor', 'vigilante' ),
1780 'analyzerQualityCritical' => __( 'Critical', 'vigilante' ),
1781 'analyzerPts' => __( 'pts', 'vigilante' ),
1782 'analyzerLearnMore' => __( 'Learn more', 'vigilante' ),
1783 'analyzerInfoAllClear' => __( 'All clear', 'vigilante' ),
1784 /* translators: %d: number of findings in an info-only category */
1785 'analyzerInfoFindings' => __( '%d findings', 'vigilante' ),
1786 ),
1787 ) );
1788
1789 // 2FA Admin assets
1790 wp_enqueue_style(
1791 'vigilante-2fa-admin',
1792 VIGILANTE_ASSETS_URL . 'css/two-factor-admin.css',
1793 array( 'vigilante-admin' ),
1794 VIGILANTE_VERSION
1795 );
1796
1797 wp_enqueue_script(
1798 'vigilante-2fa-admin',
1799 VIGILANTE_ASSETS_URL . 'js/two-factor-admin.js',
1800 array( 'jquery', 'vigilante-admin' ),
1801 VIGILANTE_VERSION,
1802 true
1803 );
1804 }
1805
1806 /**
1807 * Show admin notices
1808 */
1809 public function show_admin_notices() {
1810 // Activation notice
1811 if ( get_transient( 'vigilante_activated' ) ) {
1812 ?>
1813 <div class="notice notice-success is-dismissible vigilante-activation-notice">
1814 <p>
1815 <span class="dashicons dashicons-shield vigilante-notice-icon"></span>
1816 <strong><?php esc_html_e( 'Vigilant activated successfully!', 'vigilante' ); ?></strong>
1817 <?php esc_html_e( 'Security protection is now active.', 'vigilante' ); ?>
1818 <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante' ) ); ?>">
1819 <?php esc_html_e( 'Configure settings', 'vigilante' ); ?>
1820 </a>
1821 </p>
1822 </div>
1823 <?php
1824 delete_transient( 'vigilante_activated' );
1825 }
1826
1827 // Under Attack mode notice (non-dismissible, shown on all admin pages)
1828 $ua_status = get_option( Vigilante_Under_Attack::OPTION_NAME, array() );
1829 if ( ! empty( $ua_status['active'] ) ) {
1830 $ua_remaining = ( $ua_status['activated_at'] + $ua_status['duration'] ) - time();
1831 if ( $ua_remaining > 0 ) {
1832 $ua_hours = floor( $ua_remaining / 3600 );
1833 $ua_mins = floor( ( $ua_remaining % 3600 ) / 60 );
1834 $dashboard_url = admin_url( 'admin.php?page=vigilante' );
1835 ?>
1836 <div class="notice notice-warning vigilante-ua-notice">
1837 <p>
1838 <span class="dashicons dashicons-shield"></span>
1839 <strong><?php esc_html_e( 'Under Attack mode is active', 'vigilante' ); ?></strong>
1840 &mdash;
1841 <?php
1842 printf(
1843 /* translators: 1: Hours, 2: Minutes */
1844 esc_html__( '%1$dh %2$dm remaining.', 'vigilante' ),
1845 absint( $ua_hours ),
1846 absint( $ua_mins )
1847 );
1848 ?>
1849 <a href="<?php echo esc_url( $dashboard_url ); ?>">
1850 <?php esc_html_e( 'Go to Vigilant dashboard', 'vigilante' ); ?>
1851 </a>
1852 </p>
1853 <p>
1854 <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>
1855 </p>
1856 <?php
1857 // The cache-bypass rules could not be written (a host where
1858 // WordPress cannot write files by itself, a held lock, a
1859 // failed read-back): show them, so they can be added by hand.
1860 $ua_instance = new Vigilante_Under_Attack( $this->settings, $this->activity_log );
1861 if ( $ua_instance->cache_rules_missing() ) :
1862 ?>
1863 <p>
1864 <strong><?php esc_html_e( 'The cache-bypass rules could not be written to your .htaccess.', 'vigilante' ); ?></strong>
1865 <?php esc_html_e( 'Without them a page cache may keep serving stored pages during the attack. Add this block at the top of the .htaccess in your site root (the activity log records why it was not written):', 'vigilante' ); ?>
1866 </p>
1867 <textarea readonly rows="9" class="large-text code" onclick="this.select();"><?php echo esc_textarea( Vigilante_Under_Attack::get_cache_bypass_block() ); ?></textarea>
1868 <?php endif; ?>
1869 </div>
1870 <?php
1871 }
1872 }
1873
1874 // Proxy/CDN detection: if IP detection is set to "direct" but requests
1875 // arrive with a forwarded-for header carrying a different valid IP, the
1876 // site is very likely behind a proxy/CDN that has not been declared, so
1877 // the firewall is seeing the proxy IP for every visitor. Guide the admin.
1878 //
1879 // Two deliberate silencers (2.9.2):
1880 // - A loopback forwarded IP (::1 / 127.x) means the person browsing IS
1881 // the machine itself: that only happens in local development stacks
1882 // (Local's nginx router, Docker...), never behind a production
1883 // proxy/CDN, so the notice would be pure noise there.
1884 // - The notice is dismissible and the dismissal persists site-wide via
1885 // the vigilante_dismissed_notices option (the admin evaluated it and
1886 // decided; nagging forever helps nobody).
1887 $dismissed_notices = get_option( 'vigilante_dismissed_notices', array() );
1888 if (
1889 current_user_can( 'manage_options' )
1890 && '' === Vigilante_IP_Utils::trusted_proxy_header()
1891 && ! isset( $dismissed_notices['proxy_detection'] )
1892 ) {
1893 $remote = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
1894 $detected = '';
1895 foreach ( Vigilante_IP_Utils::trusted_header_map() as $proxy_label => $server_key ) {
1896 if ( empty( $_SERVER[ $server_key ] ) ) {
1897 continue;
1898 }
1899 $candidate = sanitize_text_field( wp_unslash( $_SERVER[ $server_key ] ) );
1900 if ( false !== strpos( $candidate, ',' ) ) {
1901 $parts = explode( ',', $candidate );
1902 $candidate = trim( $parts[0] );
1903 }
1904 if ( ! filter_var( $candidate, FILTER_VALIDATE_IP ) || $candidate === $remote ) {
1905 continue;
1906 }
1907 // Loopback forwarded IP = local development, not a real proxy.
1908 if ( '::1' === $candidate || 0 === strpos( $candidate, '127.' ) ) {
1909 continue;
1910 }
1911 $detected = $proxy_label;
1912 break;
1913 }
1914
1915 if ( '' !== $detected ) {
1916 $firewall_url = admin_url( 'admin.php?page=vigilante&tab=firewall' );
1917 $detail_message = sprintf(
1918 /* translators: %s: detected forwarded header name wrapped in a code tag. */
1919 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' ),
1920 '<code>' . esc_html( strtoupper( $detected ) ) . '</code>'
1921 );
1922 ?>
1923 <div class="notice notice-warning is-dismissible vigilante-proxy-notice">
1924 <p>
1925 <span class="dashicons dashicons-shield"></span>
1926 <strong><?php esc_html_e( 'Vigilant: this site looks like it is behind a proxy or CDN', 'vigilante' ); ?></strong>
1927 </p>
1928 <p><?php echo wp_kses_post( $detail_message ); ?></p>
1929 <p>
1930 <a href="<?php echo esc_url( $firewall_url ); ?>" class="button button-secondary">
1931 <?php esc_html_e( 'Set your proxy in Firewall settings', 'vigilante' ); ?>
1932 </a>
1933 </p>
1934 </div>
1935 <script>
1936 jQuery( function ( $ ) {
1937 $( document ).on( 'click', '.vigilante-proxy-notice .notice-dismiss', function () {
1938 $.post( ajaxurl, {
1939 action: 'vigilante_dismiss_notice',
1940 notice_id: 'proxy_detection',
1941 nonce: <?php echo wp_json_encode( wp_create_nonce( 'vigilante_dismiss_notice' ) ); ?>
1942 } );
1943 } );
1944 } );
1945 </script>
1946 <?php
1947 }
1948 }
1949 }
1950
1951 /**
1952 * Render settings page
1953 */
1954 public function render_settings_page() {
1955 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1956 $this->current_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'dashboard';
1957
1958 if ( ! array_key_exists( $this->current_tab, $this->tabs ) ) {
1959 $this->current_tab = 'dashboard';
1960 }
1961 ?>
1962 <div class="wrap vigilante-admin-wrap">
1963 <div class="vigilante-header-row">
1964 <h1 class="vigilante-page-title">
1965 <img src="<?php echo esc_url( VIGILANTE_ASSETS_URL . 'images/icon.png' ); ?>" alt="Vigilante" class="vigilante-title-icon">
1966 <?php esc_html_e( 'Vigilant', 'vigilante' ); ?>
1967 <span class="vigilante-version">v<?php echo esc_html( VIGILANTE_VERSION ); ?></span>
1968 </h1>
1969 <div class="vigilante-search-wrapper">
1970 <div class="vigilante-search-input-wrap">
1971 <span class="vigilante-search-icon dashicons dashicons-search" aria-hidden="true"></span>
1972 <input type="search" id="vigilante-settings-search" class="vigilante-settings-search" aria-label="<?php esc_attr_e( 'Search settings', 'vigilante' ); ?>" placeholder="<?php esc_attr_e( 'Search settings…', 'vigilante' ); ?>" autocomplete="off">
1973 <span class="vigilante-search-shortcut" aria-hidden="true">/</span>
1974 </div>
1975 <div id="vigilante-settings-search-results" class="vigilante-search-results" hidden role="listbox"></div>
1976 </div>
1977 </div>
1978
1979 <?php $this->render_tabs(); ?>
1980
1981 <div class="vigilante-content">
1982 <div class="vigilante-main">
1983 <?php $this->render_tab_content(); ?>
1984 </div>
1985 <div class="vigilante-sidebar">
1986 <?php $this->render_sidebar(); ?>
1987 </div>
1988 </div>
1989 </div>
1990 <?php
1991 }
1992
1993 /**
1994 * Render tabs navigation
1995 */
1996 private function render_tabs() {
1997 $options = $this->settings->get_all_options();
1998
1999 // Map tabs to modules
2000 $tab_to_module = array(
2001 'firewall' => 'firewall',
2002 'headers' => 'security_headers',
2003 'login' => 'login_security',
2004 'rest-api' => 'rest_api_security',
2005 'users' => 'user_security',
2006 'wp-hardening' => 'wp_hardening',
2007 'file-integrity' => 'file_integrity',
2008 'activity-log' => 'activity_log',
2009 );
2010 ?>
2011 <nav class="nav-tab-wrapper vigilante-nav-tabs">
2012 <?php foreach ( $this->tabs as $tab_id => $tab_name ) :
2013 $is_disabled = false;
2014 $module = isset( $tab_to_module[ $tab_id ] ) ? $tab_to_module[ $tab_id ] : null;
2015 if ( $module && empty( $options['modules'][ $module ] ) ) {
2016 $is_disabled = true;
2017 }
2018 ?>
2019 <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=' . $tab_id ) ); ?>"
2020 class="nav-tab <?php echo $this->current_tab === $tab_id ? 'nav-tab-active' : ''; ?> <?php echo $is_disabled ? 'vigilante-tab-disabled' : ''; ?>"
2021 <?php if ( $is_disabled ) : ?>title="<?php esc_attr_e( 'Module Disabled', 'vigilante' ); ?>"<?php endif; ?>>
2022 <?php echo esc_html( $tab_name ); ?>
2023 <?php if ( $is_disabled ) : ?><span class="vigilante-tab-off">OFF</span><?php endif; ?>
2024 </a>
2025 <?php endforeach; ?>
2026 </nav>
2027 <?php
2028 }
2029
2030 /**
2031 * Render current tab content
2032 */
2033 private function render_tab_content() {
2034 $method = 'render_tab_' . str_replace( '-', '_', $this->current_tab );
2035
2036 if ( method_exists( $this, $method ) ) {
2037 $this->$method();
2038 } else {
2039 $this->render_tab_coming_soon();
2040 }
2041 }
2042
2043 /**
2044 * Render coming soon placeholder
2045 */
2046 private function render_tab_coming_soon() {
2047 ?>
2048 <div class="vigilante-settings-section">
2049 <h2><?php esc_html_e( 'Coming Soon', 'vigilante' ); ?></h2>
2050 <p><?php esc_html_e( 'This section is under development.', 'vigilante' ); ?></p>
2051 </div>
2052 <?php
2053 }
2054
2055 /**
2056 * Values to display for a section that this site does not control
2057 *
2058 * On a subsite the stored options are its own copy, which nothing acts on:
2059 * wp-config.php and .htaccess are written from the main site. Painting the
2060 * local copy describes a configuration that is not running, so a subsite
2061 * admin sees a box ticked here and the constant absent from the file, or the
2062 * other way round. Read the main site's values instead, which are the ones in
2063 * force, and fall back to the local ones if they cannot be read.
2064 *
2065 * @since 2.9.8
2066 *
2067 * @param string $section Settings section.
2068 * @return array
2069 */
2070 private function get_section_for_display( $section ) {
2071 $local = $this->settings->get_section( $section );
2072
2073 if ( ! $this->shared_files_locked() ) {
2074 return $local;
2075 }
2076
2077 // shared_files_locked() is only true on multisite, where get_blog_option() exists.
2078 $main = get_blog_option( get_main_site_id(), Vigilante_Settings::OPTION_NAME, array() );
2079
2080 if ( ! is_array( $main ) || empty( $main[ $section ] ) || ! is_array( $main[ $section ] ) ) {
2081 return $local;
2082 }
2083
2084 return wp_parse_args( $main[ $section ], $local );
2085 }
2086
2087 /**
2088 * Whether the sections that write wp-config.php and .htaccess are read-only here
2089 *
2090 * True on a network when this is not the main site, or the user is not a
2091 * network administrator. See Vigilante_Settings::can_write_shared_files().
2092 *
2093 * @since 2.9.8
2094 *
2095 * @return bool
2096 */
2097 private function shared_files_locked() {
2098 return ! Vigilante_Settings::can_write_shared_files();
2099 }
2100
2101 /**
2102 * Whether this is the main site and the user cannot change what it builds the shared files from
2103 *
2104 * See Vigilante_Settings::get_main_site_file_settings(). On a subsite those
2105 * settings only act on that site, so they are never locked there.
2106 *
2107 * @since 2.11.6
2108 *
2109 * @return bool
2110 */
2111 private function main_site_files_locked() {
2112 return $this->shared_files_locked() && Vigilante_Settings::owns_shared_files();
2113 }
2114
2115 /**
2116 * Sentence added to a bulk change when some settings were left as they were
2117 *
2118 * Importing a file, applying a preset and restoring the defaults touch every
2119 * section at once, so the user is told that the shared file settings did
2120 * not move.
2121 *
2122 * @since 2.11.6
2123 *
2124 * @return string Empty when the user can change every setting.
2125 */
2126 private function locked_file_settings_message() {
2127 if ( ! Vigilante_Settings::get_locked_file_settings() ) {
2128 return '';
2129 }
2130
2131 return ' ' . __( 'The settings that end up in wp-config.php or .htaccess were left as they were.', 'vigilante' ) . ' ' . Vigilante_Settings::get_shared_files_notice();
2132 }
2133
2134 /**
2135 * Print the shared-files notice for a section that cannot be edited here
2136 *
2137 * @since 2.9.8
2138 */
2139 private function render_shared_files_notice() {
2140 if ( ! $this->shared_files_locked() ) {
2141 return;
2142 }
2143 ?>
2144 <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;">
2145 <p style="margin:0;"><?php echo esc_html( Vigilante_Settings::get_shared_files_notice() ); ?></p>
2146 </div>
2147 <?php
2148 }
2149
2150 /**
2151 * Acting on another user's account needs permission over that user
2152 *
2153 * Since 2.10.3 the handlers behind these tools ask for edit_user over the
2154 * target, which is the rule WordPress itself applies. On a network the core
2155 * grants edit_user only to network administrators, so for anybody else these
2156 * controls do nothing. Better to say so than to paint a button that silently
2157 * skips every user.
2158 *
2159 * @since 2.10.4
2160 * @return bool
2161 */
2162 private function forwarded_chain_readings() {
2163 // Shown, not decided on: the firewall resolves the address elsewhere.
2164 $chain = Vigilante_IP_Utils::trusted_forwarded_for();
2165
2166 if ( '' === $chain ) {
2167 return array();
2168 }
2169
2170 $public = array();
2171
2172 foreach ( explode( ',', $chain ) as $entry ) {
2173 $address = Vigilante_IP_Utils::unmap_ipv4( trim( $entry ) );
2174
2175 if ( filter_var( $address, FILTER_VALIDATE_IP ) && ! Vigilante_IP_Utils::is_own_network( $address ) ) {
2176 $public[] = $address;
2177 }
2178 }
2179
2180 if ( count( $public ) < 2 ) {
2181 return array();
2182 }
2183
2184 return array(
2185 'now' => Vigilante_IP_Utils::client_from_chain( $chain ),
2186 'before' => $public[0],
2187 );
2188 }
2189
2190 /**
2191 * Whether the user tools of this screen are out of reach for this user
2192 *
2193 * @return bool
2194 */
2195 private function user_actions_locked() {
2196 // On a single site edit_user maps to edit_users, which a custom role with
2197 // manage_options may lack: since 2.11.8 approving and rejecting a pending
2198 // registration ask for it, so the buttons have to say so there too.
2199 return is_multisite() ? ! current_user_can( 'manage_network_users' ) : ! current_user_can( 'edit_users' );
2200 }
2201
2202 /**
2203 * Print the notice for user tools that cannot be used from this site
2204 *
2205 * @since 2.10.4
2206 */
2207 private function render_user_actions_notice() {
2208 if ( ! $this->user_actions_locked() ) {
2209 return;
2210 }
2211 ?>
2212 <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;">
2213 <?php if ( is_multisite() ) : ?>
2214 <p style="margin:0;"><?php esc_html_e( 'These tools act on user accounts, which on a network belong to the whole network rather than to one site. WordPress reserves that to network administrators, so they are managed from the network admin.', 'vigilante' ); ?></p>
2215 <?php else : ?>
2216 <p style="margin:0;"><?php esc_html_e( 'These tools act on other user accounts, and your role cannot edit users, so they are not available to you.', 'vigilante' ); ?></p>
2217 <?php endif; ?>
2218 </div>
2219 <?php
2220 }
2221
2222 /**
2223 * Approving a change to the shared config files needs the network
2224 *
2225 * Since 2.11.3 the handler behind the Approve button asks for
2226 * manage_network_options, because the two files it approves, wp-config.php
2227 * and the root .htaccess, belong to the installation, and so does the
2228 * record of them. The button, though, went on being painted for everybody,
2229 * so the administrator of a subsite saw the warning, saw the button,
2230 * pressed it and got "Permission denied" with no explanation. That is
2231 * exactly what user_actions_locked() above exists to avoid, one release
2232 * later and one screen over. Flagged by @calzbert.
2233 *
2234 * @since 2.11.4
2235 * @return bool
2236 */
2237 private function critical_approval_locked() {
2238 return is_multisite() && ! current_user_can( 'manage_network_options' );
2239 }
2240
2241 /**
2242 * The line that replaces the Approve button where it cannot be used
2243 *
2244 * @since 2.11.4
2245 * @return string
2246 */
2247 private function critical_approval_notice() {
2248 return __( 'These files belong to the whole network rather than to this site, so a change to them is approved from the network admin.', 'vigilante' );
2249 }
2250
2251 /**
2252 * Check if module is disabled and render warning
2253 *
2254 * @param string $module_key Module key.
2255 * @return bool True if disabled.
2256 */
2257 private function render_module_disabled_notice( $module_key ) {
2258 if ( $this->settings->is_module_enabled( $module_key ) ) {
2259 return false;
2260 }
2261
2262 $module_labels = $this->settings->get_module_labels();
2263 $module_name = isset( $module_labels[ $module_key ] ) ? $module_labels[ $module_key ] : $module_key;
2264 ?>
2265 <div class="notice notice-warning vigilante-module-disabled-notice">
2266 <p>
2267 <strong><?php esc_html_e( 'Module Disabled', 'vigilante' ); ?></strong> -
2268 <?php
2269 printf(
2270 /* translators: %s: Module name */
2271 esc_html__( 'The %s module is currently disabled. Enable it from the Dashboard to use these settings.', 'vigilante' ),
2272 esc_html( $module_name )
2273 );
2274 ?>
2275 </p>
2276 <p>
2277 <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=dashboard' ) ); ?>" class="button">
2278 <?php esc_html_e( 'Go to Dashboard', 'vigilante' ); ?>
2279 </a>
2280 </p>
2281 </div>
2282 <?php
2283 return true;
2284 }
2285
2286 /**
2287 * Render the Security Analyzer (Security Check) widget + expandable full report.
2288 *
2289 * Lives in the Dashboard tab between the Configuration Score status card and
2290 * the modules grid. Uses the last persisted scan to hydrate server-side so the
2291 * first paint shows real data; "Scan now" runs the 2-phase AJAX to refresh.
2292 *
2293 * @param array $last_scan Result of Vigilante_Security_Analyzer::get_last_scan().
2294 * @param array $history Score history (oldest first).
2295 * @param array $categories_def Category metadata (slug => label/max).
2296 * @param array $analyzer_settings Settings subsection for weekly cron + email.
2297 */
2298 private function render_analyzer_widget( $last_scan, $history, $categories_def, $analyzer_settings ) {
2299 $has_data = ! empty( $last_scan['ran_at'] );
2300 $score = isset( $last_scan['score'] ) ? (int) $last_scan['score'] : 0;
2301 $grade = isset( $last_scan['grade'] ) ? (string) $last_scan['grade'] : '';
2302 $counts = isset( $last_scan['counts'] ) && is_array( $last_scan['counts'] ) ? $last_scan['counts'] : array();
2303 $ran_at_human = $has_data
2304 ? sprintf(
2305 /* translators: %s: relative time like "2 hours" */
2306 __( 'Last scan %s ago', 'vigilante' ),
2307 human_time_diff( (int) $last_scan['ran_at'], time() )
2308 )
2309 : __( 'Never scanned', 'vigilante' );
2310 $categories = isset( $last_scan['categories'] ) && is_array( $last_scan['categories'] ) ? $last_scan['categories'] : array();
2311 $weekly_enabled = ! isset( $analyzer_settings['weekly_scan_enabled'] ) || ! empty( $analyzer_settings['weekly_scan_enabled'] );
2312 $email_enabled = ! empty( $analyzer_settings['email_on_regression'] );
2313
2314 $quality = self::analyzer_quality_tag( $score );
2315 ?>
2316 <div class="vigilante-analyzer" id="vigilante-analyzer"
2317 data-has-data="<?php echo $has_data ? '1' : '0'; ?>">
2318 <div class="vigilante-analyzer-header">
2319 <div class="vigilante-analyzer-title">
2320 <h2>
2321 <span class="dashicons dashicons-shield-alt" aria-hidden="true"></span>
2322 <?php esc_html_e( 'Security Check', 'vigilante' ); ?>
2323 </h2>
2324 <p class="description">
2325 <?php esc_html_e( 'On-demand audit of what an attacker would see right now, plus 13 internal checks impossible from the outside.', 'vigilante' ); ?>
2326 </p>
2327 <p class="vigilante-analyzer-last-scan" data-role="ran-at">
2328 <span class="dashicons dashicons-clock" aria-hidden="true"></span>
2329 <?php echo esc_html( $ran_at_human ); ?>
2330 </p>
2331 </div>
2332 <div class="vigilante-analyzer-actions">
2333 <button type="button" class="button button-primary" id="vigilante-analyzer-scan">
2334 <span class="dashicons dashicons-update" aria-hidden="true"></span>
2335 <?php esc_html_e( 'Scan now', 'vigilante' ); ?>
2336 </button>
2337 </div>
2338 </div>
2339
2340 <div class="vigilante-analyzer-summary">
2341 <div class="vigilante-analyzer-score-card">
2342 <?php if ( $has_data && $grade ) : ?>
2343 <div class="vigilante-score-circle vigilante-grade-<?php echo esc_attr( strtolower( $grade ) ); ?>">
2344 <span class="vigilante-grade"><?php echo esc_html( $grade ); ?></span>
2345 <span class="vigilante-score-text"><?php echo esc_html( $score ); ?>%</span>
2346 </div>
2347 <?php else : ?>
2348 <div class="vigilante-score-circle vigilante-grade-empty">
2349 <span class="vigilante-grade">—</span>
2350 <span class="vigilante-score-text"><?php esc_html_e( 'N/A', 'vigilante' ); ?></span>
2351 </div>
2352 <?php endif; ?>
2353 <div class="vigilante-analyzer-score-meta">
2354 <p class="vigilante-analyzer-score-label">
2355 <?php esc_html_e( 'Security Score', 'vigilante' ); ?>
2356 </p>
2357 <span class="vigilante-analyzer-quality-tag vigilante-analyzer-quality-<?php echo esc_attr( $quality['slug'] ); ?>"
2358 data-role="quality-tag">
2359 <?php echo esc_html( $quality['label'] ); ?>
2360 </span>
2361 </div>
2362 </div>
2363
2364 <div class="vigilante-analyzer-counts">
2365 <span class="vigilante-analyzer-count vigilante-analyzer-count--pass">
2366 <span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>
2367 <strong data-role="pass"><?php echo esc_html( isset( $counts['pass'] ) ? $counts['pass'] : 0 ); ?></strong>
2368 <span class="vigilante-analyzer-count-label"><?php esc_html_e( 'Passed', 'vigilante' ); ?></span>
2369 </span>
2370 <span class="vigilante-analyzer-count vigilante-analyzer-count--warn">
2371 <span class="dashicons dashicons-warning" aria-hidden="true"></span>
2372 <strong data-role="warn"><?php echo esc_html( isset( $counts['warn'] ) ? $counts['warn'] : 0 ); ?></strong>
2373 <span class="vigilante-analyzer-count-label"><?php esc_html_e( 'Warnings', 'vigilante' ); ?></span>
2374 </span>
2375 <span class="vigilante-analyzer-count vigilante-analyzer-count--fail">
2376 <span class="dashicons dashicons-dismiss" aria-hidden="true"></span>
2377 <strong data-role="fail"><?php echo esc_html( isset( $counts['fail'] ) ? $counts['fail'] : 0 ); ?></strong>
2378 <span class="vigilante-analyzer-count-label"><?php esc_html_e( 'Failing', 'vigilante' ); ?></span>
2379 </span>
2380 </div>
2381
2382 <?php
2383 // Sparkline of recent scores. Require at least 3 data points so the trend
2384 // is meaningful (2 points is just a line between dots, no real trend).
2385 $hist_points = array();
2386 foreach ( $history as $h ) {
2387 $hist_points[] = (int) $h['score'];
2388 }
2389 $hist_count = count( $hist_points );
2390 if ( $hist_count >= 3 ) :
2391 $current_score = (int) end( $hist_points );
2392 $previous_score = (int) $hist_points[ $hist_count - 2 ];
2393 $delta = $current_score - $previous_score;
2394 $delta_class = $delta > 0 ? 'vigilante-analyzer-delta--up' : ( $delta < 0 ? 'vigilante-analyzer-delta--down' : 'vigilante-analyzer-delta--flat' );
2395 $delta_icon = $delta > 0 ? 'arrow-up-alt' : ( $delta < 0 ? 'arrow-down-alt' : 'minus' );
2396 if ( 0 === $delta ) {
2397 $delta_text = __( 'No change', 'vigilante' );
2398 } else {
2399 $delta_text = sprintf(
2400 /* translators: %s: signed delta, e.g. "+3" or "-5" */
2401 _n( '%s pt vs. previous scan', '%s pts vs. previous scan', abs( $delta ), 'vigilante' ),
2402 ( $delta > 0 ? '+' : '' ) . (int) $delta
2403 );
2404 }
2405 ?>
2406 <div class="vigilante-analyzer-sparkline-wrap">
2407 <div class="vigilante-analyzer-sparkline-head">
2408 <span class="vigilante-analyzer-sparkline-label">
2409 <?php
2410 echo esc_html( sprintf(
2411 /* translators: %d: number of scans */
2412 _n( 'Score trend (last %d scan)', 'Score trend (last %d scans)', $hist_count, 'vigilante' ),
2413 $hist_count
2414 ) );
2415 ?>
2416 </span>
2417 <span class="vigilante-analyzer-delta <?php echo esc_attr( $delta_class ); ?>">
2418 <span class="dashicons dashicons-<?php echo esc_attr( $delta_icon ); ?>" aria-hidden="true"></span>
2419 <?php echo esc_html( $delta_text ); ?>
2420 </span>
2421 </div>
2422 <div class="vigilante-analyzer-sparkline" data-role="sparkline"
2423 data-points="<?php echo esc_attr( wp_json_encode( $hist_points ) ); ?>">
2424 <?php echo self::sparkline_svg( $hist_points ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Safe SVG from helper ?>
2425 </div>
2426 </div>
2427 <?php elseif ( $has_data ) : ?>
2428 <div class="vigilante-analyzer-sparkline-wrap vigilante-analyzer-sparkline-wrap--placeholder">
2429 <span class="vigilante-analyzer-sparkline-label">
2430 <?php esc_html_e( 'Score trend', 'vigilante' ); ?>
2431 </span>
2432 <p class="vigilante-analyzer-sparkline-hint">
2433 <span class="dashicons dashicons-chart-line" aria-hidden="true"></span>
2434 <?php
2435 $needed = 3 - $hist_count;
2436 echo esc_html( sprintf(
2437 /* translators: %d: number of additional scans needed */
2438 _n( '%d more scan needed to show a trend.', '%d more scans needed to show a trend.', $needed, 'vigilante' ),
2439 $needed
2440 ) );
2441 ?>
2442 </p>
2443 </div>
2444 <?php endif; ?>
2445 </div>
2446
2447 <div class="vigilante-analyzer-toggle-row">
2448 <button type="button" class="button-link vigilante-analyzer-toggle" aria-expanded="false">
2449 <?php esc_html_e( 'Show detailed breakdown', 'vigilante' ); ?>
2450 <span class="vigilante-analyzer-toggle-chevron" aria-hidden="true"></span>
2451 </button>
2452 </div>
2453
2454 <div class="vigilante-analyzer-details" hidden>
2455 <div class="vigilante-analyzer-categories" data-role="categories">
2456 <?php foreach ( $categories_def as $slug => $meta ) :
2457 $cat = isset( $categories[ $slug ] ) ? $categories[ $slug ] : array();
2458 $earned = isset( $cat['earned'] ) ? (int) $cat['earned'] : 0;
2459 // Always use the declared meta as the source of truth for the maximum.
2460 // The cached scan may carry an old max if a check was added/removed
2461 // between releases (see 2.6.1: closed_plugins raised internal from 22 to 28
2462 // but cached scans still reported max=22 until reset).
2463 $cat_max = (int) $meta['max'];
2464 $info_only = ! empty( $meta['info_only'] ) || 0 === (int) $meta['max'];
2465 $cat_pct = $cat_max > 0 ? (int) round( ( $earned / $cat_max ) * 100 ) : 0;
2466 $checks = isset( $cat['checks'] ) ? (array) $cat['checks'] : array();
2467 $cat_counts = isset( $cat['counts'] ) && is_array( $cat['counts'] ) ? $cat['counts'] : array( 'pass' => 0, 'warn' => 0, 'fail' => 0, 'info' => 0 );
2468 $cat_quality = self::analyzer_quality_tag( $cat_pct );
2469 $info_count = isset( $cat_counts['info'] ) ? (int) $cat_counts['info'] : 0;
2470 ?>
2471 <details class="vigilante-analyzer-category<?php echo $info_only ? ' vigilante-analyzer-category--info' : ''; ?>"
2472 data-category="<?php echo esc_attr( $slug ); ?>"
2473 data-info-only="<?php echo $info_only ? '1' : '0'; ?>">
2474 <summary class="vigilante-analyzer-category-summary">
2475 <span class="vigilante-analyzer-category-chevron" aria-hidden="true"></span>
2476 <span class="vigilante-analyzer-category-label"><?php echo esc_html( $meta['label'] ); ?></span>
2477 <?php if ( $info_only ) : ?>
2478 <span class="vigilante-analyzer-category-quality vigilante-analyzer-quality-info"
2479 data-role="category-quality"
2480 title="<?php esc_attr_e( 'Informational — does not affect the security score.', 'vigilante' ); ?>">
2481 <span class="dashicons dashicons-info-outline" aria-hidden="true"></span>
2482 <?php esc_html_e( 'Informational', 'vigilante' ); ?>
2483 </span>
2484 <?php else :
2485 $passed_count = (int) ( $cat_counts['pass'] ?? 0 );
2486 $scored_total = $passed_count
2487 + (int) ( $cat_counts['warn'] ?? 0 )
2488 + (int) ( $cat_counts['fail'] ?? 0 );
2489 ?>
2490 <span class="vigilante-analyzer-category-quality vigilante-analyzer-quality-<?php echo esc_attr( $cat_quality['slug'] ); ?>"
2491 data-role="category-quality">
2492 <span data-role="category-quality-label"><?php echo esc_html( $cat_quality['label'] ); ?></span>
2493 <span class="vigilante-analyzer-category-quality-sep" aria-hidden="true">·</span>
2494 <span class="vigilante-analyzer-category-tests" data-role="category-tests"
2495 data-passed="<?php echo esc_attr( $passed_count ); ?>"
2496 data-total="<?php echo esc_attr( $scored_total ); ?>">
2497 <?php
2498 echo esc_html( sprintf(
2499 /* translators: 1: tests passed, 2: total tests in this category */
2500 __( '%1$d/%2$d tests', 'vigilante' ),
2501 $passed_count,
2502 $scored_total
2503 ) );
2504 ?>
2505 </span>
2506 </span>
2507 <?php endif; ?>
2508 <?php if ( $info_only ) : ?>
2509 <span class="vigilante-analyzer-category-states" data-role="category-states">
2510 <?php if ( $info_count > 0 ) : ?>
2511 <span class="vigilante-analyzer-category-state vigilante-analyzer-category-state--info" title="<?php esc_attr_e( 'Informational', 'vigilante' ); ?>">
2512 <span data-role="state-info"><?php echo esc_html( $info_count ); ?></span><span class="dashicons dashicons-info-outline" aria-hidden="true"></span>
2513 </span>
2514 <?php endif; ?>
2515 </span>
2516 <?php endif; ?>
2517 <?php if ( ! $info_only ) : ?>
2518 <span class="vigilante-analyzer-category-score">
2519 <span data-role="earned"><?php echo esc_html( $earned ); ?></span><span class="vigilante-analyzer-category-score-sep">/</span><?php echo esc_html( $cat_max ); ?>
2520 <span class="vigilante-analyzer-category-score-unit"><?php esc_html_e( 'pts', 'vigilante' ); ?></span>
2521 </span>
2522 <span class="vigilante-analyzer-category-bar" aria-hidden="true">
2523 <span class="vigilante-analyzer-category-bar-fill vigilante-analyzer-category-bar-fill--<?php echo esc_attr( $cat_quality['slug'] ); ?>"
2524 style="width: <?php echo esc_attr( $cat_pct ); ?>%"
2525 data-role="category-bar"></span>
2526 </span>
2527 <?php else :
2528 $info_warn = isset( $cat_counts['warn'] ) ? (int) $cat_counts['warn'] : 0;
2529 $info_fail = isset( $cat_counts['fail'] ) ? (int) $cat_counts['fail'] : 0;
2530 $info_issues = $info_warn + $info_fail;
2531 if ( $info_issues > 0 ) : ?>
2532 <span class="vigilante-analyzer-category-status vigilante-analyzer-category-status--attention" data-role="info-status">
2533 <span class="dashicons dashicons-warning" aria-hidden="true"></span>
2534 <?php
2535 echo esc_html( sprintf(
2536 /* translators: %d: number of findings */
2537 _n( '%d finding', '%d findings', $info_issues, 'vigilante' ),
2538 $info_issues
2539 ) );
2540 ?>
2541 </span>
2542 <?php else : ?>
2543 <span class="vigilante-analyzer-category-status vigilante-analyzer-category-status--clear" data-role="info-status">
2544 <span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>
2545 <?php esc_html_e( 'All clear', 'vigilante' ); ?>
2546 </span>
2547 <?php endif; ?>
2548 <?php endif; ?>
2549 </summary>
2550 <ul class="vigilante-analyzer-check-list" data-role="check-list">
2551 <?php if ( empty( $checks ) ) : ?>
2552 <li class="vigilante-analyzer-check-empty">
2553 <?php esc_html_e( 'No data yet — run a scan to populate this category.', 'vigilante' ); ?>
2554 </li>
2555 <?php else : ?>
2556 <?php foreach ( $checks as $c ) : ?>
2557 <?php echo self::render_analyzer_check_row( $c ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped inside helper ?>
2558 <?php endforeach; ?>
2559 <?php endif; ?>
2560 </ul>
2561 </details>
2562 <?php endforeach; ?>
2563 </div>
2564
2565 <div class="vigilante-analyzer-weekly">
2566 <h3><?php esc_html_e( 'Automatic weekly scan', 'vigilante' ); ?></h3>
2567 <p class="description">
2568 <?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' ); ?>
2569 </p>
2570 <label class="vigilante-analyzer-toggle-option">
2571 <input type="checkbox"
2572 name="security_analyzer[weekly_scan_enabled]"
2573 value="1"
2574 <?php checked( $weekly_enabled ); ?>>
2575 <?php esc_html_e( 'Run a weekly automatic scan', 'vigilante' ); ?>
2576 </label>
2577 <label class="vigilante-analyzer-toggle-option">
2578 <input type="checkbox"
2579 name="security_analyzer[email_on_regression]"
2580 value="1"
2581 <?php checked( $email_enabled ); ?>>
2582 <?php esc_html_e( 'Email me when the score drops significantly', 'vigilante' ); ?>
2583 </label>
2584 </div>
2585 </div>
2586 </div>
2587 <?php
2588 }
2589
2590 /**
2591 * Map a 0-100 percentage to a quality tag { label, slug } aligned with the
2592 * Dashboard grade palette (a/b/c/d/e).
2593 *
2594 * @param int $pct 0..100.
2595 * @return array{label:string,slug:string}
2596 */
2597 public static function analyzer_quality_tag( $pct ) {
2598 $pct = max( 0, min( 100, (int) $pct ) );
2599 // "Excellent" is reserved for a perfect score — a single missing point drops to Good.
2600 if ( 100 === $pct ) {
2601 return array( 'label' => __( 'Excellent', 'vigilante' ), 'slug' => 'a' );
2602 }
2603 if ( $pct >= 70 ) {
2604 return array( 'label' => __( 'Good', 'vigilante' ), 'slug' => 'b' );
2605 }
2606 if ( $pct >= 50 ) {
2607 return array( 'label' => __( 'Fair', 'vigilante' ), 'slug' => 'c' );
2608 }
2609 if ( $pct >= 30 ) {
2610 return array( 'label' => __( 'Poor', 'vigilante' ), 'slug' => 'd' );
2611 }
2612 return array( 'label' => __( 'Critical', 'vigilante' ), 'slug' => 'e' );
2613 }
2614
2615 /**
2616 * Render a single analyzer check row (used both server-side and via JS template).
2617 *
2618 * @param array $check Check result array (from Vigilante_SA_Check_Result::to_array()).
2619 * @return string HTML (escaped).
2620 */
2621 private static function render_analyzer_check_row( $check ) {
2622 $id = isset( $check['id'] ) ? $check['id'] : '';
2623 $state = isset( $check['state'] ) ? $check['state'] : 'skip';
2624 $label = isset( $check['label'] ) ? $check['label'] : '';
2625 $detail = isset( $check['detail'] ) ? $check['detail'] : '';
2626 $score = isset( $check['score'] ) ? (int) $check['score'] : 0;
2627 $max = isset( $check['max'] ) ? (int) $check['max'] : 0;
2628 $fix_link = isset( $check['fix_link'] ) ? $check['fix_link'] : '';
2629
2630 $icons = array(
2631 'pass' => 'yes-alt',
2632 'warn' => 'warning',
2633 'fail' => 'dismiss',
2634 'info' => 'info',
2635 'skip' => 'minus',
2636 );
2637 $icon = isset( $icons[ $state ] ) ? $icons[ $state ] : 'minus';
2638
2639 $html = '<li class="vigilante-analyzer-check vigilante-analyzer-check--' . esc_attr( $state ) . '"';
2640 $html .= ' data-check-id="' . esc_attr( $id ) . '">';
2641 $html .= '<span class="vigilante-analyzer-check-icon dashicons dashicons-' . esc_attr( $icon ) . '" aria-hidden="true"></span>';
2642 $html .= '<div class="vigilante-analyzer-check-body">';
2643 $html .= '<div class="vigilante-analyzer-check-label">';
2644 $html .= '<span>' . esc_html( $label ) . '</span>';
2645 if ( $max > 0 && 'info' !== $state && 'skip' !== $state ) {
2646 $html .= '<span class="vigilante-analyzer-check-score">'
2647 . esc_html( $score . '/' . $max )
2648 . ' <span class="vigilante-analyzer-check-score-unit">' . esc_html__( 'pts', 'vigilante' ) . '</span>'
2649 . '</span>';
2650 }
2651 $html .= '</div>';
2652 if ( $detail ) {
2653 $html .= '<p class="vigilante-analyzer-check-detail">' . esc_html( $detail ) . '</p>';
2654 }
2655 if ( $fix_link && in_array( $state, array( 'fail', 'warn' ), true ) ) {
2656 $html .= '<a href="' . esc_url( $fix_link ) . '" class="vigilante-analyzer-fix-link">'
2657 . esc_html__( 'Go to setting', 'vigilante' )
2658 . '<span class="vigilante-analyzer-fix-arrow" aria-hidden="true">&rarr;</span></a>';
2659 } elseif ( $fix_link && 'info' === $state ) {
2660 // Info rows (e.g. DNSBL lookups) get an external "Learn more" link instead.
2661 $is_external = 0 === strpos( $fix_link, 'http' );
2662 $html .= '<a href="' . esc_url( $fix_link ) . '" class="vigilante-analyzer-fix-link"'
2663 . ( $is_external ? ' target="_blank" rel="noopener noreferrer"' : '' ) . '>'
2664 . esc_html__( 'Learn more', 'vigilante' )
2665 . '<span class="vigilante-analyzer-fix-arrow" aria-hidden="true">&rarr;</span></a>';
2666 }
2667 $html .= '</div>';
2668 $html .= '</li>';
2669 return $html;
2670 }
2671
2672 /**
2673 * Build a minimal SVG sparkline for the score history.
2674 *
2675 * @param int[] $points Score values (0..100), oldest to newest.
2676 * @return string SVG markup.
2677 */
2678 private static function sparkline_svg( $points ) {
2679 $points = array_map( 'intval', (array) $points );
2680 $count = count( $points );
2681 if ( $count < 2 ) {
2682 return '';
2683 }
2684 $width = 280;
2685 $height = 60;
2686 $padding = 4;
2687
2688 $usable_w = $width - ( $padding * 2 );
2689 $usable_h = $height - ( $padding * 2 );
2690
2691 $step = $usable_w / max( 1, $count - 1 );
2692 $max = 100; // Fixed scale — scores are 0..100.
2693
2694 $coords = array();
2695 foreach ( $points as $i => $v ) {
2696 $x = $padding + ( $i * $step );
2697 $y = $padding + ( $usable_h - ( ( $v / $max ) * $usable_h ) );
2698 $coords[] = round( $x, 2 ) . ',' . round( $y, 2 );
2699 }
2700 $path = 'M ' . implode( ' L ', $coords );
2701 $last = end( $points );
2702 $last_x = $padding + ( ( $count - 1 ) * $step );
2703 $last_y = $padding + ( $usable_h - ( ( $last / $max ) * $usable_h ) );
2704
2705 $svg = '<svg viewBox="0 0 ' . $width . ' ' . $height . '" preserveAspectRatio="none" role="img" aria-label="' . esc_attr__( 'Security Score history', 'vigilante' ) . '" focusable="false">';
2706 $svg .= '<path d="' . esc_attr( $path ) . '" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
2707 $svg .= '<circle cx="' . esc_attr( round( $last_x, 2 ) ) . '" cy="' . esc_attr( round( $last_y, 2 ) ) . '" r="3" fill="currentColor"/>';
2708 $svg .= '</svg>';
2709 return $svg;
2710 }
2711
2712 /**
2713 * Render dashboard tab
2714 */
2715 private function render_tab_dashboard() {
2716 $options = $this->settings->get_all_options();
2717 $module_labels = $this->settings->get_module_labels();
2718 $module_descriptions = $this->settings->get_module_descriptions();
2719 $presets = $this->settings->get_presets();
2720 $active_preset = get_option( 'vigilante_active_preset', '' );
2721
2722 // Calculate security score with more factors
2723 $security_score = $this->calculate_security_score( $options );
2724
2725 // Security Analyzer (v2.1.0) — hydrate widget with the last persisted scan, if any.
2726 if ( ! class_exists( 'Vigilante_Security_Analyzer' ) ) {
2727 require_once VIGILANTE_INCLUDES_DIR . 'class-security-analyzer.php';
2728 }
2729 $analyzer_instance = new Vigilante_Security_Analyzer( $this->settings, $this->activity_log );
2730 $analyzer_last_scan = $analyzer_instance->get_last_scan();
2731 $analyzer_history = $analyzer_instance->get_score_history();
2732 $analyzer_categories_def = Vigilante_Security_Analyzer::get_categories();
2733 $analyzer_settings = isset( $options['security_analyzer'] ) ? $options['security_analyzer'] : array();
2734 ?>
2735 <div class="vigilante-dashboard">
2736 <div class="vigilante-status-card">
2737 <h2><?php esc_html_e( 'Configuration Score', 'vigilante' ); ?></h2>
2738 <p class="vigilante-score-kind description">
2739 <?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' ); ?>
2740 </p>
2741 <div class="vigilante-security-score">
2742 <?php
2743 // Grade thresholds: A (90+), B (70-89), C (50-69), D (30-49), E (0-29)
2744 if ( $security_score >= 90 ) {
2745 $grade = 'A';
2746 } elseif ( $security_score >= 70 ) {
2747 $grade = 'B';
2748 } elseif ( $security_score >= 50 ) {
2749 $grade = 'C';
2750 } elseif ( $security_score >= 30 ) {
2751 $grade = 'D';
2752 } else {
2753 $grade = 'E';
2754 }
2755 ?>
2756 <div class="vigilante-score-circle vigilante-grade-<?php echo esc_attr( strtolower( $grade ) ); ?>">
2757 <span class="vigilante-grade"><?php echo esc_html( $grade ); ?></span>
2758 <span class="vigilante-score-text"><?php echo esc_html( $security_score ); ?>%</span>
2759 </div>
2760 <div class="vigilante-config-status">
2761 <?php if ( $active_preset && isset( $presets[ $active_preset ] ) ) : ?>
2762 <span class="vigilante-preset-badge vigilante-preset-<?php echo esc_attr( $active_preset ); ?>">
2763 <?php echo esc_html( $presets[ $active_preset ]['name'] ); ?>
2764 </span>
2765 <?php else : ?>
2766 <span class="vigilante-preset-badge vigilante-preset-custom">
2767 <?php esc_html_e( 'Custom Configuration', 'vigilante' ); ?>
2768 </span>
2769 <?php endif; ?>
2770 </div>
2771 </div>
2772
2773 <?php
2774 $recommendations = $this->get_security_recommendations( $options );
2775 if ( ! empty( $recommendations ) ) :
2776 ?>
2777 <div class="vigilante-recommendations">
2778 <h4><?php esc_html_e( 'Recommendations', 'vigilante' ); ?></h4>
2779 <ul class="vigilante-recommendations-grid">
2780 <?php foreach ( $recommendations as $rec ) : ?>
2781 <li>
2782 <span class="dashicons dashicons-<?php echo esc_attr( $rec['icon'] ); ?> vigilante-priority-<?php echo esc_attr( $rec['priority'] ); ?>"></span>
2783 <?php echo esc_html( $rec['message'] ); ?>
2784 <?php if ( ! empty( $rec['tab'] ) ) : ?>
2785 <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>
2786 <?php endif; ?>
2787 </li>
2788 <?php endforeach; ?>
2789 </ul>
2790 </div>
2791 <?php endif; ?>
2792 </div>
2793
2794 <?php $this->render_analyzer_widget( $analyzer_last_scan, $analyzer_history, $analyzer_categories_def, $analyzer_settings ); ?>
2795
2796 <div class="vigilante-modules-grid">
2797 <h2 id="vigilante-section-dashboard-modules"><?php esc_html_e( 'Security Modules', 'vigilante' ); ?></h2>
2798 <p class="description"><?php esc_html_e( 'Enable or disable security modules. Each module controls a tab with detailed settings.', 'vigilante' ); ?></p>
2799 <div class="vigilante-modules-list">
2800 <?php foreach ( $options['modules'] as $module => $enabled ) :
2801 $label = isset( $module_labels[ $module ] ) ? $module_labels[ $module ] : ucwords( str_replace( '_', ' ', $module ) );
2802 $description = isset( $module_descriptions[ $module ] ) ? $module_descriptions[ $module ] : '';
2803 $vg_module_locked = $this->main_site_files_locked() && in_array( $module, Vigilante_Settings::get_main_site_file_settings()['modules'], true );
2804 ?>
2805 <div class="vigilante-module-item <?php echo $enabled ? 'enabled' : 'disabled'; ?>">
2806 <div class="vigilante-module-header">
2807 <span class="vigilante-module-status"></span>
2808 <span class="vigilante-module-name"><?php echo esc_html( $label ); ?></span>
2809 <label class="vigilante-toggle">
2810 <?php
2811 /* translators: %s: Security module name, for example Firewall. */
2812 $toggle_label = sprintf( __( 'Enable %s', 'vigilante' ), $label );
2813 ?>
2814 <input type="checkbox"
2815 name="modules[<?php echo esc_attr( $module ); ?>]"
2816 value="1"
2817 <?php checked( $enabled ); ?>
2818 <?php disabled( $vg_module_locked ); ?>
2819 aria-label="<?php echo esc_attr( $toggle_label ); ?>"
2820 data-module="<?php echo esc_attr( $module ); ?>">
2821 <span class="vigilante-toggle-slider"></span>
2822 </label>
2823 </div>
2824 <?php if ( $description ) : ?>
2825 <p class="vigilante-module-desc"><?php echo esc_html( $description ); ?></p>
2826 <?php endif; ?>
2827 <?php if ( $vg_module_locked ) : ?>
2828 <p class="vigilante-module-desc"><?php esc_html_e( 'On the main site of a network this module also writes files every site shares, so only a network administrator can switch it.', 'vigilante' ); ?></p>
2829 <?php endif; ?>
2830 </div>
2831 <?php endforeach; ?>
2832 </div>
2833 </div>
2834
2835 <div class="vigilante-presets-card">
2836 <h2><?php esc_html_e( 'Quick Configuration Presets', 'vigilante' ); ?></h2>
2837 <p><?php esc_html_e( 'Apply a preset to quickly set up recommended settings for standard or maximum security level.', 'vigilante' ); ?></p>
2838 <div class="vigilante-presets-grid">
2839 <?php foreach ( $presets as $preset_id => $preset ) :
2840 $is_active = ( $active_preset === $preset_id );
2841 ?>
2842 <div class="vigilante-preset-card <?php echo $is_active ? 'vigilante-preset-active' : ''; ?>">
2843 <?php if ( $is_active ) : ?>
2844 <span class="vigilante-active-indicator"><?php esc_html_e( 'Active', 'vigilante' ); ?></span>
2845 <?php endif; ?>
2846 <h3><?php echo esc_html( $preset['name'] ); ?></h3>
2847 <p><?php echo esc_html( $preset['description'] ); ?></p>
2848 <button type="button" class="button vigilante-preset-btn <?php echo $is_active ? 'button-primary' : ''; ?>" data-preset="<?php echo esc_attr( $preset_id ); ?>">
2849 <?php esc_html_e( 'Apply Preset', 'vigilante' ); ?>
2850 </button>
2851 </div>
2852 <?php endforeach; ?>
2853
2854 <?php
2855 // Under Attack mode card
2856 $under_attack = new Vigilante_Under_Attack( $this->settings, $this->activity_log );
2857 $ua_active = $under_attack->is_active();
2858 $ua_remaining = $under_attack->get_remaining_time();
2859 $ua_remaining_hours = floor( $ua_remaining / 3600 );
2860 $ua_remaining_mins = floor( ( $ua_remaining % 3600 ) / 60 );
2861 ?>
2862 <div class="vigilante-preset-card vigilante-under-attack-card <?php echo $ua_active ? 'vigilante-under-attack-active' : ''; ?>">
2863 <h3 id="vigilante-section-dashboard-under-attack">
2864 <span class="dashicons dashicons-shield"></span>
2865 <?php esc_html_e( 'Under Attack', 'vigilante' ); ?>
2866 </h3>
2867 <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>
2868 <?php if ( $ua_active ) : ?>
2869 <div class="vigilante-ua-countdown" data-expires="<?php echo esc_attr( $under_attack->get_status()['activated_at'] + $under_attack->get_status()['duration'] ); ?>">
2870 <span class="dashicons dashicons-clock"></span>
2871 <span class="vigilante-ua-time">
2872 <?php
2873 printf(
2874 /* translators: 1: Hours, 2: Minutes */
2875 esc_html__( '%1$dh %2$dm remaining', 'vigilante' ),
2876 absint( $ua_remaining_hours ),
2877 absint( $ua_remaining_mins )
2878 );
2879 ?>
2880 </span>
2881 </div>
2882 <button type="button" class="button vigilante-ua-btn vigilante-ua-deactivate">
2883 <?php esc_html_e( 'Deactivate', 'vigilante' ); ?>
2884 </button>
2885 <?php else : ?>
2886 <button type="button" class="button vigilante-ua-btn vigilante-ua-activate">
2887 <?php esc_html_e( 'Activate for 4 hours', 'vigilante' ); ?>
2888 </button>
2889 <?php endif; ?>
2890 </div>
2891 </div>
2892 </div>
2893 </div>
2894 <?php
2895 }
2896
2897 /**
2898 * Render tools tab
2899 */
2900 private function render_tab_tools() {
2901 // Get current settings for notification summary
2902 $email_options = $this->settings->get_section( 'email' );
2903 $login_options = $this->settings->get_section( 'login_security' );
2904 $user_options = $this->settings->get_section( 'user_security' );
2905 $fi_options = $this->settings->get_section( 'file_integrity' );
2906 $alerts_options = $this->settings->get_section( 'audit_alerts' );
2907 $monitoring = $user_options['admin_monitoring'] ?? array();
2908 $registration = $user_options['registration_approval'] ?? array();
2909 $current_admin = get_option( 'admin_email' );
2910 $send_to_admin = ! isset( $email_options['send_to_admin_email'] ) || ! empty( $email_options['send_to_admin_email'] );
2911 $additional_raw = $email_options['additional_recipients'] ?? array();
2912 $additional = is_array( $additional_raw ) ? implode( "\n", $additional_raw ) : trim( $additional_raw );
2913 ?>
2914
2915 <!-- Notification Settings -->
2916 <div id="vigilante-section-tools-notifications" class="vigilante-settings-section vigilante-notification-section">
2917 <h2><?php esc_html_e( 'Notification settings', 'vigilante' ); ?></h2>
2918 <p><?php esc_html_e( 'Configure who receives all administrative email notifications from Vigilant. Individual notifications are enabled in their respective tabs.', 'vigilante' ); ?></p>
2919
2920 <div class="vigilante-notification-layout">
2921
2922 <!-- Left column: Recipients settings -->
2923 <div class="vigilante-notification-settings">
2924 <form class="vigilante-settings-form" data-section="email">
2925
2926 <table class="form-table vigilante-compact-form">
2927 <tr>
2928 <th scope="row"><?php esc_html_e( 'WordPress Admin Email', 'vigilante' ); ?></th>
2929 <td>
2930 <label>
2931 <input type="checkbox" name="email[send_to_admin_email]" value="1" <?php checked( $send_to_admin ); ?>>
2932 <?php
2933 printf(
2934 /* translators: %s: Admin email address */
2935 esc_html__( 'Send to admin email (%s)', 'vigilante' ),
2936 '<code>' . esc_html( $current_admin ) . '</code>'
2937 );
2938 ?>
2939 </label>
2940 </td>
2941 </tr>
2942 <tr>
2943 <th scope="row"><label for="vigilante-f-email-additional-recipients"><?php esc_html_e( 'Additional Recipients', 'vigilante' ); ?></label></th>
2944 <td>
2945 <textarea id="vigilante-f-email-additional-recipients" name="email[additional_recipients]" rows="3" class="large-text code" placeholder="maintenance@example.com&#10;security@example.com"><?php echo esc_textarea( $additional ); ?></textarea>
2946 <p class="description"><?php esc_html_e( 'One email per line.', 'vigilante' ); ?></p>
2947 </td>
2948 </tr>
2949 <tr>
2950 <th scope="row"><?php esc_html_e( 'Plugin Deactivation', 'vigilante' ); ?></th>
2951 <td>
2952 <label>
2953 <input type="checkbox" name="email[send_deactivation_email]" value="1" <?php checked( ! empty( $email_options['send_deactivation_email'] ) ); ?>>
2954 <?php esc_html_e( 'Send email when Vigilant is deactivated', 'vigilante' ); ?>
2955 </label>
2956 </td>
2957 </tr>
2958 </table>
2959
2960 <p class="submit vigilante-notification-submit">
2961 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
2962 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
2963 </button>
2964 <button type="button" class="button vigilante-test-email-btn" data-original-text="<?php esc_attr_e( 'Send test email', 'vigilante' ); ?>">
2965 <?php esc_html_e( 'Send test email', 'vigilante' ); ?>
2966 </button>
2967 <span class="vigilante-test-email-result" style="margin-left:8px;vertical-align:middle;"></span>
2968 </p>
2969 <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>
2970
2971 </form>
2972 </div>
2973
2974 <!-- Right column: Active notifications summary -->
2975 <div class="vigilante-notification-summary">
2976 <h4><?php esc_html_e( 'Active notifications', 'vigilante' ); ?></h4>
2977
2978 <table class="widefat striped">
2979 <thead>
2980 <tr>
2981 <th><?php esc_html_e( 'Notification', 'vigilante' ); ?></th>
2982 <th style="width: 1%; white-space: nowrap; text-align: center;"><?php esc_html_e( 'Status', 'vigilante' ); ?></th>
2983 <th style="width: 50px; text-align: center;"></th>
2984 </tr>
2985 </thead>
2986 <tbody>
2987 <?php
2988 $notifications = array(
2989 array(
2990 'label' => __( 'Login lockout', 'vigilante' ),
2991 'active' => ! empty( $login_options['notify_on_lockout'] ),
2992 'tab' => 'login',
2993 ),
2994 array(
2995 'label' => __( 'Administrator login', 'vigilante' ),
2996 'active' => ! empty( $login_options['notify_on_admin_login'] ),
2997 'tab' => 'login',
2998 ),
2999 array(
3000 'label' => __( 'New administrator created', 'vigilante' ),
3001 'active' => ! empty( $monitoring['alert_new_admin'] ),
3002 'tab' => 'users',
3003 ),
3004 array(
3005 'label' => __( 'Administrator email changed', 'vigilante' ),
3006 'active' => ! empty( $monitoring['alert_admin_email_change'] ),
3007 'tab' => 'users',
3008 ),
3009 array(
3010 'label' => __( 'Permission elevation', 'vigilante' ),
3011 'active' => ! empty( $monitoring['alert_permission_elevation'] ),
3012 'tab' => 'users',
3013 ),
3014 array(
3015 'label' => __( 'Admin password changed', 'vigilante' ),
3016 'active' => ! empty( $monitoring['alert_admin_password_change'] ),
3017 'tab' => 'users',
3018 ),
3019 array(
3020 'label' => __( 'Registration pending approval', 'vigilante' ),
3021 'active' => ! empty( $registration['enabled'] ) && ! empty( $registration['notify_admin'] ),
3022 'tab' => 'users',
3023 ),
3024 array(
3025 'label' => __( 'File integrity scan report', 'vigilante' ),
3026 'active' => ( $fi_options['notify_level'] ?? 'disabled' ) !== 'disabled',
3027 'tab' => 'file-integrity',
3028 ),
3029 array(
3030 'label' => __( 'File integrity instant alert', 'vigilante' ),
3031 'active' => ! empty( $fi_options['instant_alert'] ),
3032 'tab' => 'file-integrity',
3033 ),
3034 array(
3035 'label' => __( 'Audit alert: immediate', 'vigilante' ),
3036 'active' => Vigilante_Audit_Alerts::immediate_is_active( $alerts_options ),
3037 'tab' => 'activity-log',
3038 'anchor' => 'vigilante-section-audit-alerts',
3039 ),
3040 array(
3041 'label' => __( 'Audit alert: threshold', 'vigilante' ),
3042 'active' => Vigilante_Audit_Alerts::threshold_is_active( $alerts_options ),
3043 'tab' => 'activity-log',
3044 'anchor' => 'vigilante-section-audit-alerts',
3045 ),
3046 array(
3047 'label' => __( 'Under Attack mode', 'vigilante' ),
3048 'active' => true,
3049 'tab' => '',
3050 'note' => __( 'Always active', 'vigilante' ),
3051 ),
3052 array(
3053 'label' => __( 'Plugin deactivation', 'vigilante' ),
3054 'active' => ! empty( $email_options['send_deactivation_email'] ),
3055 'tab' => 'tools',
3056 ),
3057 );
3058
3059 foreach ( $notifications as $notif ) :
3060 $status_class = $notif['active'] ? 'vigilante-status-active' : 'vigilante-status-inactive';
3061 $status_label = $notif['active'] ? __( 'Active', 'vigilante' ) : __( 'Inactive', 'vigilante' );
3062 if ( ! empty( $notif['note'] ) ) {
3063 $status_label = $notif['note'];
3064 }
3065 ?>
3066 <tr>
3067 <td><?php echo esc_html( $notif['label'] ); ?></td>
3068 <td style="text-align: center; white-space: nowrap;">
3069 <span class="<?php echo esc_attr( $status_class ); ?>"><?php echo esc_html( $status_label ); ?></span>
3070 </td>
3071 <td style="text-align: center;">
3072 <?php if ( ! empty( $notif['tab'] ) ) : ?>
3073 <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=' . $notif['tab'] ) . ( ! empty( $notif['anchor'] ) ? '#' . $notif['anchor'] : '' ) ); ?>" class="button button-small">
3074 <span class="dashicons dashicons-admin-generic" style="font-size: 14px; line-height: 1.8;"></span>
3075 </a>
3076 <?php else : ?>
3077 &mdash;
3078 <?php endif; ?>
3079 </td>
3080 </tr>
3081 <?php endforeach; ?>
3082 </tbody>
3083 </table>
3084 </div>
3085
3086 </div>
3087 </div>
3088
3089 <h2 id="vigilante-section-tools-main" class="vigilante-tools-heading"><?php esc_html_e( 'Tools', 'vigilante' ); ?></h2>
3090
3091 <?php
3092 // Warn that during Under Attack mode the export/import operate against
3093 // the temporary hardened config and any imported changes will be
3094 // reverted when the mode ends.
3095 $ua_status = get_option( Vigilante_Under_Attack::OPTION_NAME, array() );
3096 if ( ! empty( $ua_status['active'] ) ) :
3097 ?>
3098 <div class="vigilante-ua-tools-notice-wrap">
3099 <div class="notice notice-warning inline vigilante-ua-tools-notice">
3100 <p>
3101 <strong><?php esc_html_e( 'Under Attack mode is active.', 'vigilante' ); ?></strong>
3102 <?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' ); ?>
3103 </p>
3104 </div>
3105 </div>
3106 <?php
3107 endif;
3108 ?>
3109
3110 <div class="vigilante-tools-grid">
3111 <div class="vigilante-tool-card">
3112 <h3><?php esc_html_e( 'Export Settings', 'vigilante' ); ?></h3>
3113 <p><?php esc_html_e( 'Download your current security settings as a JSON file.', 'vigilante' ); ?></p>
3114 <button type="button" class="button vigilante-export-settings">
3115 <?php esc_html_e( 'Export Settings', 'vigilante' ); ?>
3116 </button>
3117 </div>
3118
3119 <div class="vigilante-tool-card">
3120 <h3><?php esc_html_e( 'Import Settings', 'vigilante' ); ?></h3>
3121 <p><?php esc_html_e( 'Import settings from a previously exported JSON file.', 'vigilante' ); ?></p>
3122 <input type="file" id="vigilante-import-file" aria-label="<?php esc_attr_e( 'Configuration file to import', 'vigilante' ); ?>" accept=".json" style="display: none;">
3123 <button type="button" class="button vigilante-import-settings">
3124 <?php esc_html_e( 'Import Settings', 'vigilante' ); ?>
3125 </button>
3126 </div>
3127
3128 <div class="vigilante-tool-card">
3129 <h3><?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?></h3>
3130 <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>
3131 <button type="button" class="button vigilante-reset-settings" style="color: #a00;">
3132 <?php esc_html_e( 'Reset All Settings', 'vigilante' ); ?>
3133 </button>
3134 </div>
3135
3136 <div class="vigilante-tool-card">
3137 <h3><?php esc_html_e( 'Download Config Backup', 'vigilante' ); ?></h3>
3138 <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>
3139 <?php if ( $this->shared_files_locked() ) : ?>
3140 <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>
3141 <?php else : ?>
3142 <button type="button" class="button vigilante-create-backup">
3143 <?php esc_html_e( 'Download Backup', 'vigilante' ); ?>
3144 </button>
3145 <?php endif; ?>
3146 </div>
3147
3148 <?php if ( ! $this->shared_files_locked() ) : ?>
3149
3150 <div class="vigilante-tool-card vigilante-tool-card-wide">
3151 <h3><?php esc_html_e( 'Database Backup', 'vigilante' ); ?></h3>
3152 <p><?php esc_html_e( 'Download a backup of your database as a ZIP file. Select which tables to include.', 'vigilante' ); ?></p>
3153 <button type="button" class="button vigilante-db-backup-toggle">
3154 <?php esc_html_e( 'Download Database Backup', 'vigilante' ); ?>
3155 </button>
3156
3157 <div class="vigilante-db-backup-panel" style="display: none;">
3158 <div class="vigilante-db-tables-loading">
3159 <span class="spinner is-active"></span>
3160 <?php esc_html_e( 'Loading tables...', 'vigilante' ); ?>
3161 </div>
3162
3163 <div class="vigilante-db-tables-content" style="display: none;">
3164 <div class="vigilante-db-tables-controls">
3165 <label>
3166 <input type="checkbox" id="vigilante-db-select-all" checked>
3167 <strong><?php esc_html_e( 'Select / deselect all', 'vigilante' ); ?></strong>
3168 </label>
3169 <span class="vigilante-db-tables-info"></span>
3170 </div>
3171
3172 <div class="vigilante-db-tables-group">
3173 <h4><?php esc_html_e( 'WordPress core tables', 'vigilante' ); ?></h4>
3174 <div class="vigilante-db-tables-list" id="vigilante-db-core-tables"></div>
3175 </div>
3176
3177 <div class="vigilante-db-tables-group" id="vigilante-db-other-group" style="display: none;">
3178 <h4><?php esc_html_e( 'Plugin and custom tables', 'vigilante' ); ?></h4>
3179 <div class="vigilante-db-tables-list" id="vigilante-db-other-tables"></div>
3180 </div>
3181
3182 <div class="vigilante-db-backup-actions">
3183 <button type="button" class="button button-primary vigilante-db-backup-download">
3184 <?php esc_html_e( 'Download Backup (.zip)', 'vigilante' ); ?>
3185 </button>
3186 </div>
3187 </div>
3188 </div>
3189 </div>
3190 <?php else : ?>
3191 <div class="vigilante-tool-card vigilante-tool-card-wide">
3192 <h3><?php esc_html_e( 'Database Backup', 'vigilante' ); ?></h3>
3193 <p><?php esc_html_e( 'Download a backup of your database as a ZIP file. Select which tables to include.', 'vigilante' ); ?></p>
3194 <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>
3195 </div>
3196 <?php endif; ?>
3197 </div>
3198 <?php
3199 }
3200
3201 /**
3202 * Render firewall tab
3203 */
3204 private function render_tab_firewall() {
3205 $is_disabled = $this->render_module_disabled_notice( 'firewall' );
3206 $options = $this->settings->get_section( 'firewall' );
3207 ?>
3208 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="firewall" <?php echo $is_disabled ? 'inert' : ''; ?>>
3209 <div id="vigilante-section-firewall-main" class="vigilante-settings-section">
3210 <h2>
3211 <?php esc_html_e( 'Firewall Protection', 'vigilante' ); ?>
3212 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
3213 </h2>
3214 <p><?php esc_html_e( 'PHP-based request filtering. Analyzes each request before WordPress loads.', 'vigilante' ); ?></p>
3215 <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;">
3216 <p style="margin:0;">
3217 <?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' ); ?>
3218 </p>
3219 </div>
3220
3221 <?php $vg_main_locked = $this->main_site_files_locked(); ?>
3222 <?php if ( $vg_main_locked ) : ?>
3223 <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;">
3224 <p style="margin:0;"><?php esc_html_e( 'On the main site of a network, blocking bad bots and bad query strings, the visitor IP detection and the two whitelists also build the .htaccess rules every site shares, so only a network administrator can change them.', 'vigilante' ); ?></p>
3225 </div>
3226 <?php endif; ?>
3227
3228 <table class="form-table">
3229 <tr>
3230 <th scope="row"><?php esc_html_e( 'Block Bad Query Strings', 'vigilante' ); ?></th>
3231 <td>
3232 <label>
3233 <input type="checkbox" name="firewall[block_bad_query_strings]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( ! empty( $options['block_bad_query_strings'] ) ); ?>>
3234 <?php esc_html_e( 'Block malicious query string patterns', 'vigilante' ); ?>
3235 </label>
3236 </td>
3237 </tr>
3238 <tr>
3239 <th scope="row"><?php esc_html_e( 'SQL Injection Protection', 'vigilante' ); ?></th>
3240 <td>
3241 <label>
3242 <input type="checkbox" name="firewall[block_sql_injection]" value="1" <?php checked( ! empty( $options['block_sql_injection'] ) ); ?>>
3243 <?php esc_html_e( 'Block SQL injection attempts', 'vigilante' ); ?>
3244 </label>
3245 </td>
3246 </tr>
3247 <tr>
3248 <th scope="row"><?php esc_html_e( 'XSS Protection', 'vigilante' ); ?></th>
3249 <td>
3250 <label>
3251 <input type="checkbox" name="firewall[block_xss_attacks]" value="1" <?php checked( ! empty( $options['block_xss_attacks'] ) ); ?>>
3252 <?php esc_html_e( 'Block cross-site scripting attacks', 'vigilante' ); ?>
3253 </label>
3254 </td>
3255 </tr>
3256 <tr>
3257 <th scope="row"><?php esc_html_e( 'File Inclusion Protection', 'vigilante' ); ?></th>
3258 <td>
3259 <label>
3260 <input type="checkbox" name="firewall[block_file_inclusion]" value="1" <?php checked( ! empty( $options['block_file_inclusion'] ) ); ?>>
3261 <?php esc_html_e( 'Block local/remote file inclusion attempts', 'vigilante' ); ?>
3262 </label>
3263 </td>
3264 </tr>
3265 <tr>
3266 <th scope="row"><?php esc_html_e( 'Directory Traversal Protection', 'vigilante' ); ?></th>
3267 <td>
3268 <label>
3269 <input type="checkbox" name="firewall[block_directory_traversal]" value="1" <?php checked( ! empty( $options['block_directory_traversal'] ) ); ?>>
3270 <?php esc_html_e( 'Block path traversal attempts', 'vigilante' ); ?>
3271 </label>
3272 </td>
3273 </tr>
3274 <tr>
3275 <th scope="row"><?php esc_html_e( 'Block Bad Bots', 'vigilante' ); ?></th>
3276 <td>
3277 <label>
3278 <input type="checkbox" name="firewall[block_bad_bots]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( ! empty( $options['block_bad_bots'] ) ); ?>>
3279 <?php esc_html_e( 'Block known malicious bots and scanners', 'vigilante' ); ?>
3280 </label>
3281 </td>
3282 </tr>
3283 </table>
3284
3285 <h3><?php esc_html_e( 'Rate Limiting', 'vigilante' ); ?></h3>
3286 <table class="form-table">
3287 <tr>
3288 <th scope="row"><?php esc_html_e( 'Enable Rate Limiting', 'vigilante' ); ?></th>
3289 <td>
3290 <label>
3291 <input type="checkbox" name="firewall[rate_limiting][enabled]" value="1" <?php checked( ! empty( $options['rate_limiting']['enabled'] ) ); ?>>
3292 <?php esc_html_e( 'Limit requests per IP address', 'vigilante' ); ?>
3293 </label>
3294 </td>
3295 </tr>
3296 <tr>
3297 <th scope="row"><label for="vigilante-f-firewall-rate-limiting-requests-per-minute"><?php esc_html_e( 'Requests per Minute', 'vigilante' ); ?></label></th>
3298 <td>
3299 <input id="vigilante-f-firewall-rate-limiting-requests-per-minute" 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">
3300 <p class="description">
3301 <?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' ); ?>
3302 </p>
3303 </td>
3304 </tr>
3305 <tr>
3306 <th scope="row"><label for="vigilante-f-firewall-rate-limiting-block-duration"><?php esc_html_e( 'Block Duration (seconds)', 'vigilante' ); ?></label></th>
3307 <td>
3308 <input id="vigilante-f-firewall-rate-limiting-block-duration" 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">
3309 </td>
3310 </tr>
3311 <tr>
3312 <th scope="row"><?php esc_html_e( 'Progressive Blocking', 'vigilante' ); ?></th>
3313 <td>
3314 <label>
3315 <input type="checkbox" name="firewall[rate_limiting][progressive]" value="1" <?php checked( ! empty( $options['rate_limiting']['progressive'] ) ); ?>>
3316 <?php esc_html_e( 'Double block duration on each repeat offense', 'vigilante' ); ?>
3317 </label>
3318 <p class="description">
3319 <?php
3320 $base = absint( $options['rate_limiting']['block_duration'] ?? 300 );
3321 printf(
3322 /* translators: 1: First block duration, 2: Second, 3: Third */
3323 esc_html__( 'Example: %1$s → %2$s → %3$s and so on, up to the maximum.', 'vigilante' ),
3324 esc_html( human_time_diff( 0, $base ) ),
3325 esc_html( human_time_diff( 0, $base * 2 ) ),
3326 esc_html( human_time_diff( 0, $base * 4 ) )
3327 );
3328 ?>
3329 </p>
3330 </td>
3331 </tr>
3332 <tr>
3333 <th scope="row"><label for="vigilante-f-firewall-rate-limiting-max-block-duration"><?php esc_html_e( 'Maximum Block Duration', 'vigilante' ); ?></label></th>
3334 <td>
3335 <select id="vigilante-f-firewall-rate-limiting-max-block-duration" name="firewall[rate_limiting][max_block_duration]">
3336 <?php
3337 $max_options = array(
3338 3600 => __( '1 hour', 'vigilante' ),
3339 21600 => __( '6 hours', 'vigilante' ),
3340 43200 => __( '12 hours', 'vigilante' ),
3341 86400 => __( '24 hours', 'vigilante' ),
3342 604800 => __( '7 days', 'vigilante' ),
3343 );
3344 $current_max = absint( $options['rate_limiting']['max_block_duration'] ?? 86400 );
3345 foreach ( $max_options as $val => $label ) :
3346 ?>
3347 <option value="<?php echo esc_attr( $val ); ?>" <?php selected( $current_max, $val ); ?>>
3348 <?php echo esc_html( $label ); ?>
3349 </option>
3350 <?php endforeach; ?>
3351 </select>
3352 <p class="description"><?php esc_html_e( 'Upper limit for progressive blocking.', 'vigilante' ); ?></p>
3353 </td>
3354 </tr>
3355 </table>
3356
3357 <?php
3358 // Currently blocked IPs from rate limiting
3359 $active_blocks = Vigilante_Firewall::get_active_blocks();
3360 if ( ! empty( $active_blocks ) ) :
3361 ?>
3362 <div class="vigilante-settings-section vigilante-lockout-section" style="margin-top:20px;">
3363 <h3><?php esc_html_e( 'Currently Blocked IPs', 'vigilante' ); ?></h3>
3364 <table class="wp-list-table widefat fixed striped" style="max-width:800px;">
3365 <thead>
3366 <tr>
3367 <th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th>
3368 <th><?php esc_html_e( 'Blocked', 'vigilante' ); ?></th>
3369 <th><?php esc_html_e( 'Expires in', 'vigilante' ); ?></th>
3370 <th><?php esc_html_e( 'Strikes', 'vigilante' ); ?></th>
3371 <th><?php esc_html_e( 'Action', 'vigilante' ); ?></th>
3372 </tr>
3373 </thead>
3374 <tbody>
3375 <?php foreach ( $active_blocks as $blocked_ip => $block_data ) : ?>
3376 <tr>
3377 <td><code><?php echo esc_html( $blocked_ip ); ?></code></td>
3378 <td><?php echo esc_html( human_time_diff( $block_data['blocked_at'] ) . ' ' . __( 'ago', 'vigilante' ) ); ?></td>
3379 <td><?php echo esc_html( human_time_diff( time(), $block_data['expires'] ) ); ?></td>
3380 <td><?php echo esc_html( $block_data['strikes'] ?? 1 ); ?></td>
3381 <td>
3382 <button type="button" class="button button-small vigilante-unblock-firewall-ip"
3383 data-ip="<?php echo esc_attr( $blocked_ip ); ?>">
3384 <?php esc_html_e( 'Unblock', 'vigilante' ); ?>
3385 </button>
3386 </td>
3387 </tr>
3388 <?php endforeach; ?>
3389 </tbody>
3390 </table>
3391 </div>
3392 <?php endif; ?>
3393
3394 <?php
3395 // Since 2.11.8 X-Forwarded-For is read from its end, where the proxy
3396 // writes. The administrator's own request shows whether that end is
3397 // a CDN or a balancer for everybody here. Cross review of 2.11.8.
3398 $xff_readings = $this->forwarded_chain_readings();
3399 if ( $xff_readings ) :
3400 ?>
3401 <div id="vigilante-xff-chain-notice" class="notice notice-warning inline" style="margin:10px 0 16px;padding:8px 12px;">
3402 <p style="margin:0;">
3403 <?php
3404 printf(
3405 /* translators: 1: address Vigilant reads now, 2: address earlier versions read */
3406 esc_html__( 'Your own request reaches the site with more than one public address in X-Forwarded-For. Vigilant reads the last one, %1$s, which is the one your proxy added; up to version 2.11.7 it read the first one, %2$s, which a visitor can write. If %1$s belongs to a CDN or a load balancer rather than to you, every visitor shares it for rate limiting, login lockouts and the IP lists: choose the header of that CDN in Visitor IP detection, such as CF-Connecting-IP for Cloudflare.', 'vigilante' ),
3407 esc_html( $xff_readings['now'] ),
3408 esc_html( $xff_readings['before'] )
3409 );
3410 ?>
3411 </p>
3412 </div>
3413 <?php endif; ?>
3414
3415 <h3><?php esc_html_e( 'IP Lists', 'vigilante' ); ?></h3>
3416 <p class="description">
3417 <?php
3418 printf(
3419 /* translators: %s: Current visitor IP address */
3420 esc_html__( 'Your current IP address: %s', 'vigilante' ),
3421 '<code>' . esc_html( $this->database->get_client_ip() ) . '</code>'
3422 );
3423 ?>
3424 </p>
3425 <table class="form-table">
3426 <tr>
3427 <th scope="row"><label for="vigilante-f-firewall-trusted-proxy-header"><?php esc_html_e( 'Visitor IP detection', 'vigilante' ); ?></label></th>
3428 <td>
3429 <?php $proxy_header = $options['trusted_proxy_header'] ?? ''; ?>
3430 <select id="vigilante-f-firewall-trusted-proxy-header" name="firewall[trusted_proxy_header]" <?php disabled( $vg_main_locked ); ?>>
3431 <option value="" <?php selected( $proxy_header, '' ); ?>><?php esc_html_e( 'Direct connection, only REMOTE_ADDR (recommended)', 'vigilante' ); ?></option>
3432 <option value="cf-connecting-ip" <?php selected( $proxy_header, 'cf-connecting-ip' ); ?>><?php esc_html_e( 'Behind Cloudflare (CF-Connecting-IP)', 'vigilante' ); ?></option>
3433 <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>
3434 <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>
3435 </select>
3436 <p class="description">
3437 <?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' ); ?>
3438 </p>
3439 </td>
3440 </tr>
3441 <tr>
3442 <th scope="row"><label for="vigilante-f-firewall-trusted-proxies"><?php esc_html_e( 'Trusted proxy IPs', 'vigilante' ); ?></label></th>
3443 <td>
3444 <textarea id="vigilante-f-firewall-trusted-proxies" name="firewall[trusted_proxies]" rows="3" class="large-text code" placeholder="10.0.0.0/8&#10;192.168.1.1"><?php echo esc_textarea( implode( "\n", $options['trusted_proxies'] ?? array() ) ); ?></textarea>
3445 <p class="description">
3446 <?php esc_html_e( 'Only used with a forwarded header selected above. One IP or CIDR range per line: the addresses your proxy or load balancer connects from. The forwarded header is accepted only from these. Left empty, Vigilant accepts it from your own private network, and for Cloudflare from Cloudflare\'s own ranges automatically.', 'vigilante' ); ?>
3447 <?php if ( in_array( $proxy_header, array( 'x-forwarded-for', 'x-real-ip' ), true ) && empty( $options['trusted_proxies'] ) ) : ?>
3448 <br><strong><?php esc_html_e( 'The header above is trusted but no proxy IPs are set. If your proxy or load balancer connects from a public address, add it here, or the header is ignored for safety and every visitor is seen as that proxy.', 'vigilante' ); ?></strong>
3449 <?php endif; ?>
3450 </p>
3451 </td>
3452 </tr>
3453 <tr>
3454 <th scope="row"><label for="vigilante-f-firewall-ip-whitelist"><?php esc_html_e( 'IP Whitelist', 'vigilante' ); ?></label></th>
3455 <td>
3456 <textarea id="vigilante-f-firewall-ip-whitelist" name="firewall[ip_whitelist]" <?php disabled( $vg_main_locked ); ?> 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>
3457 <p class="description">
3458 <?php esc_html_e( 'One IP per line. These IPs bypass the firewall checks, and they also reach wp-admin when the login URL is hidden, so remote managers such as MainWP or ManageWP are not turned away with a 404. The hidden login form itself stays hidden for every IP, this one included.', 'vigilante' ); ?>
3459 <br>
3460 <?php
3461 printf(
3462 /* translators: 1: opening <code>, 2: closing </code>. Placeholders wrap the IP, CIDR and wildcard examples. */
3463 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' ),
3464 '<code>',
3465 '</code>'
3466 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded.
3467 ?>
3468 </p>
3469 </td>
3470 </tr>
3471 <tr>
3472 <th scope="row"><label for="vigilante-f-firewall-ip-blacklist"><?php esc_html_e( 'IP Blacklist', 'vigilante' ); ?></label></th>
3473 <td>
3474 <textarea id="vigilante-f-firewall-ip-blacklist" 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>
3475 <p class="description">
3476 <?php esc_html_e( 'One IP per line. These IPs will be blocked immediately.', 'vigilante' ); ?>
3477 <br>
3478 <?php
3479 printf(
3480 /* translators: 1: opening <code>, 2: closing </code>. Placeholders wrap the IP, CIDR and wildcard examples. */
3481 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' ),
3482 '<code>',
3483 '</code>'
3484 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded.
3485 ?>
3486 </p>
3487 </td>
3488 </tr>
3489 </table>
3490
3491 <h3><?php esc_html_e( 'User-Agent Lists', 'vigilante' ); ?></h3>
3492 <p><?php esc_html_e( 'Partial matching: enter a keyword and any User-Agent containing it will be matched.', 'vigilante' ); ?></p>
3493 <table class="form-table">
3494 <tr>
3495 <th scope="row"><label for="vigilante-f-firewall-ua-whitelist"><?php esc_html_e( 'User-Agent Whitelist', 'vigilante' ); ?></label></th>
3496 <td>
3497 <textarea id="vigilante-f-firewall-ua-whitelist" name="firewall[ua_whitelist]" <?php disabled( $vg_main_locked ); ?> rows="4" class="large-text code"><?php echo esc_textarea( implode( "\n", $options['ua_whitelist'] ?? array() ) ); ?></textarea>
3498 <p class="description"><?php esc_html_e( 'One User-Agent per line. These will bypass all firewall checks. Example: ManageWP, MainWP, UptimeRobot.', 'vigilante' ); ?></p>
3499 </td>
3500 </tr>
3501 <tr>
3502 <th scope="row"><label for="vigilante-f-firewall-ua-blacklist"><?php esc_html_e( 'User-Agent Blacklist', 'vigilante' ); ?></label></th>
3503 <td>
3504 <textarea id="vigilante-f-firewall-ua-blacklist" name="firewall[ua_blacklist]" rows="4" class="large-text code"><?php echo esc_textarea( implode( "\n", $options['ua_blacklist'] ?? array() ) ); ?></textarea>
3505 <p class="description"><?php esc_html_e( 'One User-Agent per line. These will be blocked immediately.', 'vigilante' ); ?></p>
3506 </td>
3507 </tr>
3508 </table>
3509 </div>
3510
3511 <?php
3512 $vg_shared_locked = $this->shared_files_locked();
3513 // Paint what is actually in force, not this site's unused copy.
3514 $vg_local_options = $options;
3515 $options = $this->get_section_for_display( 'firewall' );
3516 ?>
3517 <?php $this->render_shared_files_notice(); ?>
3518 <div id="vigilante-section-firewall-server" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
3519 <h2>
3520 <?php esc_html_e( 'Server Protection', 'vigilante' ); ?>
3521 <span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span>
3522 </h2>
3523 <p><?php esc_html_e( 'Server-level rules for Apache/LiteSpeed. These rules are processed before PHP.', 'vigilante' ); ?></p>
3524
3525 <table class="form-table">
3526 <tr id="field-disable-directory-browsing">
3527 <th scope="row"><?php esc_html_e( 'Directory Browsing', 'vigilante' ); ?></th>
3528 <td>
3529 <label>
3530 <input type="checkbox" name="firewall[disable_directory_browsing]" value="1" <?php checked( ! empty( $options['disable_directory_browsing'] ) ); ?>>
3531 <?php esc_html_e( 'Disable directory listing (Options -Indexes)', 'vigilante' ); ?>
3532 </label>
3533 </td>
3534 </tr>
3535 <tr>
3536 <th scope="row"><?php esc_html_e( 'Protect wp-config.php', 'vigilante' ); ?></th>
3537 <td>
3538 <label>
3539 <input type="checkbox" name="firewall[protect_wp_config]" value="1" <?php checked( ! empty( $options['protect_wp_config'] ) ); ?>>
3540 <?php esc_html_e( 'Block direct HTTP access to wp-config.php', 'vigilante' ); ?>
3541 </label>
3542 </td>
3543 </tr>
3544 <tr id="field-protect-wp-cron">
3545 <th scope="row"><?php esc_html_e( 'Protect wp-cron.php', 'vigilante' ); ?></th>
3546 <td>
3547 <label>
3548 <input type="checkbox" name="firewall[protect_wp_cron]" value="1" <?php checked( ! empty( $options['protect_wp_cron'] ) ); ?>>
3549 <?php esc_html_e( 'Block direct HTTP access to wp-cron.php (prevents cron-spam DoS abuse)', 'vigilante' ); ?>
3550 </label>
3551 <p class="description"><?php
3552 printf(
3553 /* translators: 1: opening <strong>, 2: closing </strong> */
3554 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' ),
3555 '<strong>',
3556 '</strong>',
3557 '<code>',
3558 '</code>'
3559 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded.
3560 ?></p>
3561 </td>
3562 </tr>
3563 <tr>
3564 <th scope="row"><?php esc_html_e( 'Protect wp-includes', 'vigilante' ); ?></th>
3565 <td>
3566 <label>
3567 <input type="checkbox" name="firewall[protect_wp_includes]" value="1" <?php checked( ! empty( $options['protect_wp_includes'] ) ); ?>>
3568 <?php esc_html_e( 'Block direct access to PHP files in wp-includes', 'vigilante' ); ?>
3569 </label>
3570 </td>
3571 </tr>
3572 <tr>
3573 <th scope="row"><?php esc_html_e( 'PHP in Uploads', 'vigilante' ); ?></th>
3574 <td>
3575 <label>
3576 <input type="checkbox" name="firewall[protect_uploads_php]" value="1" <?php checked( ! empty( $options['protect_uploads_php'] ) ); ?>>
3577 <?php esc_html_e( 'Block PHP execution in wp-content/uploads', 'vigilante' ); ?>
3578 </label>
3579 </td>
3580 </tr>
3581 <tr>
3582 <th scope="row"><?php esc_html_e( 'Sensitive Files', 'vigilante' ); ?></th>
3583 <td>
3584 <label>
3585 <input type="checkbox" name="firewall[protect_sensitive_files]" value="1" <?php checked( ! empty( $options['protect_sensitive_files'] ) ); ?>>
3586 <?php esc_html_e( 'Block access to .sql, .bak, .log, .ini, readme.html, license.txt, licencia.txt', 'vigilante' ); ?>
3587 </label>
3588 </td>
3589 </tr>
3590 <tr>
3591 <th scope="row"><?php esc_html_e( 'Limit HTTP Methods', 'vigilante' ); ?></th>
3592 <td>
3593 <label>
3594 <input type="checkbox" name="firewall[limit_http_methods]" value="1" <?php checked( ! empty( $options['limit_http_methods'] ) ); ?>>
3595 <?php esc_html_e( 'Allow only GET, POST, HEAD (blocks PUT, DELETE, TRACE, etc.)', 'vigilante' ); ?>
3596 </label>
3597 </td>
3598 </tr>
3599 </table>
3600 </div>
3601 <?php $options = $vg_local_options; ?>
3602
3603 <p class="submit vigilante-submit-buttons">
3604 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
3605 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
3606 </button>
3607 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
3608 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
3609 </button>
3610 </p>
3611 </form>
3612 <?php
3613 }
3614
3615 /**
3616 * Render login security tab
3617 */
3618 private function render_tab_login() {
3619 $is_disabled = $this->render_module_disabled_notice( 'login_security' );
3620 $options = $this->settings->get_section( 'login_security' );
3621 $lockouts = $this->database->get_active_lockouts();
3622 ?>
3623 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="login_security" <?php echo $is_disabled ? 'inert' : ''; ?>>
3624 <div id="vigilante-section-login-main" class="vigilante-settings-section">
3625 <h2>
3626 <?php esc_html_e( 'Login Protection', 'vigilante' ); ?>
3627 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
3628 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
3629 </h2>
3630 <p><?php esc_html_e( 'Brute force protection and WordPress login hardening.', 'vigilante' ); ?></p>
3631
3632 <table class="form-table">
3633 <tr id="field-max-attempts">
3634 <th scope="row"><label for="vigilante-f-login-security-max-attempts"><?php esc_html_e( 'Max Login Attempts', 'vigilante' ); ?></label></th>
3635 <td>
3636 <input id="vigilante-f-login-security-max-attempts" type="number" name="login_security[max_attempts]" value="<?php echo esc_attr( $options['max_attempts'] ?? 5 ); ?>" min="1" max="20" class="small-text">
3637 <p class="description"><?php esc_html_e( 'Number of failed attempts before lockout.', 'vigilante' ); ?></p>
3638 </td>
3639 </tr>
3640 <tr>
3641 <th scope="row"><label for="vigilante-f-login-security-lockout-duration"><?php esc_html_e( 'Lockout Duration', 'vigilante' ); ?></label></th>
3642 <td>
3643 <input id="vigilante-f-login-security-lockout-duration" type="number" name="login_security[lockout_duration]" value="<?php echo esc_attr( ( $options['lockout_duration'] ?? 1800 ) / 60 ); ?>" min="1" max="1440" class="small-text">
3644 <?php esc_html_e( 'minutes', 'vigilante' ); ?>
3645 </td>
3646 </tr>
3647 <tr>
3648 <th scope="row"><?php esc_html_e( 'Progressive Lockout', 'vigilante' ); ?></th>
3649 <td>
3650 <label>
3651 <input type="checkbox" name="login_security[lockout_increment]" value="1" <?php checked( ! empty( $options['lockout_increment'] ) ); ?>>
3652 <?php esc_html_e( 'Double lockout duration for repeat offenders', 'vigilante' ); ?>
3653 </label>
3654 </td>
3655 </tr>
3656 <tr>
3657 <th scope="row"><?php esc_html_e( 'Hide Login Errors', 'vigilante' ); ?></th>
3658 <td>
3659 <label>
3660 <input type="checkbox" name="login_security[hide_login_errors]" value="1" <?php checked( ! empty( $options['hide_login_errors'] ) ); ?>>
3661 <?php esc_html_e( 'Show generic error message instead of specific errors', 'vigilante' ); ?>
3662 </label>
3663 </td>
3664 </tr>
3665 <tr>
3666 <th scope="row"><?php esc_html_e( 'Disable Application Passwords', 'vigilante' ); ?></th>
3667 <td>
3668 <label>
3669 <input type="checkbox" name="login_security[disable_application_passwords]" value="1" <?php checked( ! empty( $options['disable_application_passwords'] ) ); ?>>
3670 <?php esc_html_e( 'Disable WordPress application passwords feature', 'vigilante' ); ?>
3671 </label>
3672 </td>
3673 </tr>
3674 </table>
3675
3676 <h3><?php esc_html_e( 'Custom Login URL', 'vigilante' ); ?></h3>
3677 <table class="form-table">
3678 <tr id="field-custom-login-url">
3679 <th scope="row"><?php esc_html_e( 'Login URL Slug', 'vigilante' ); ?></th>
3680 <td>
3681 <code><?php echo esc_url( home_url( '/' ) ); ?></code>
3682 <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' ); ?>">
3683 <p class="description">
3684 <?php esc_html_e( '&#9432; Leave empty to use default wp-login.php. Use only lowercase letters, numbers and hyphens.', 'vigilante' ); ?>
3685 </p>
3686 <div class="vigilante-login-url-preview" <?php echo empty( $options['custom_login_url'] ) ? 'style="display:none;"' : ''; ?>>
3687 <p class="description">
3688 <strong><?php esc_html_e( 'Your login URL:', 'vigilante' ); ?></strong>
3689 <code class="vigilante-login-url-display"><?php echo esc_url( home_url( sanitize_title( $options['custom_login_url'] ?? '' ) . '/' ) ); ?></code>
3690 </p>
3691 <p class="description">
3692 <?php esc_html_e( 'Direct access to wp-login.php and wp-admin will return a 404 error for non-logged users.', 'vigilante' ); ?>
3693 </p>
3694 <p class="description">
3695 <?php esc_html_e( 'An IP in the firewall whitelist is still allowed into wp-admin, so remote managers keep working, but it does not get the login form: the hidden URL is the only way in for everyone.', 'vigilante' ); ?>
3696 </p>
3697 </div>
3698 </td>
3699 </tr>
3700 </table>
3701
3702 <div class="vigilante-login-url-notify-wrapper <?php echo empty( $options['custom_login_url'] ) ? 'vigilante-login-url-notify-disabled' : ''; ?>">
3703 <table class="form-table">
3704 <tr>
3705 <th scope="row"><?php esc_html_e( 'Notify users', 'vigilante' ); ?></th>
3706 <td>
3707 <label>
3708 <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 ); ?>>
3709 <?php esc_html_e( 'Notify affected users when the login URL changes', 'vigilante' ); ?>
3710 </label>
3711 <div class="vigilante-login-url-send-notification" style="margin-top:12px;">
3712 <button type="button" id="vigilante_notify_login_url" class="button">
3713 <?php esc_html_e( 'Send notification now', 'vigilante' ); ?>
3714 </button>
3715 <span class="vigilante-login-url-notify-status"></span>
3716 <p class="description"><?php esc_html_e( 'Sends the new URL to administrators, editors, authors, and contributors.', 'vigilante' ); ?></p>
3717 </div>
3718 </td>
3719 </tr>
3720 </table>
3721 </div>
3722
3723 <?php $this->render_2fa_settings( $options ); ?>
3724
3725 <h3><?php esc_html_e( 'Notifications', 'vigilante' ); ?></h3>
3726 <table class="form-table">
3727 <tr>
3728 <th scope="row"><?php esc_html_e( 'Notify on Lockout', 'vigilante' ); ?></th>
3729 <td>
3730 <label>
3731 <input type="checkbox" name="login_security[notify_on_lockout]" value="1" <?php checked( ! empty( $options['notify_on_lockout'] ) ); ?>>
3732 <?php esc_html_e( 'Send email when an IP is locked out', 'vigilante' ); ?>
3733 </label>
3734 </td>
3735 </tr>
3736 <tr>
3737 <th scope="row"><?php esc_html_e( 'Notify on Admin Login', 'vigilante' ); ?></th>
3738 <td>
3739 <label>
3740 <input type="checkbox" name="login_security[notify_on_admin_login]" value="1" <?php checked( ! empty( $options['notify_on_admin_login'] ) ); ?>>
3741 <?php esc_html_e( 'Send email when an administrator logs in', 'vigilante' ); ?>
3742 </label>
3743 </td>
3744 </tr>
3745 </table>
3746
3747 <p class="description">
3748 <?php
3749 printf(
3750 /* translators: %s: Link to notification settings */
3751 esc_html__( '&#9432; Notifications are sent to the recipients configured in %s.', 'vigilante' ),
3752 '<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>'
3753 );
3754 ?>
3755 </p>
3756 </div>
3757
3758 <p class="submit vigilante-submit-buttons">
3759 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
3760 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
3761 </button>
3762 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
3763 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
3764 </button>
3765 </p>
3766 </form>
3767
3768 <?php $this->render_lockout_info_section( $options, $lockouts ); ?>
3769 <?php
3770 }
3771
3772 /**
3773 * Render lockout information and blocked IPs section
3774 *
3775 * @param array $options Login security options.
3776 * @param array $lockouts Currently locked out IPs.
3777 */
3778 private function render_lockout_info_section( $options, $lockouts ) {
3779 $max_attempts = absint( $options['max_attempts'] ?? 5 );
3780 $lockout_duration = absint( $options['lockout_duration'] ?? 1800 );
3781 $lockout_increment = ! empty( $options['lockout_increment'] );
3782 $max_lockout = absint( $options['max_lockout_duration'] ?? 86400 );
3783 $two_factor = $options['two_factor'] ?? array();
3784 $two_factor_enabled = ! empty( $two_factor['enabled'] );
3785 ?>
3786 <div class="vigilante-settings-section vigilante-lockout-section">
3787 <h2 id="vigilante-section-login-status"><?php esc_html_e( 'Login Protection Status', 'vigilante' ); ?></h2>
3788
3789 <table class="form-table">
3790 <tr>
3791 <th scope="row"><?php esc_html_e( 'Current settings', 'vigilante' ); ?></th>
3792 <td>
3793 <?php
3794 printf(
3795 /* translators: 1: Maximum login attempts, 2: Lockout duration in minutes */
3796 esc_html__( 'After %1$d failed login attempts, the IP address is blocked for %2$d minutes.', 'vigilante' ),
3797 absint( $max_attempts ),
3798 absint( ceil( $lockout_duration / 60 ) )
3799 );
3800
3801 if ( $lockout_increment ) {
3802 echo '<br>';
3803 printf(
3804 /* translators: %d: Maximum lockout duration in hours */
3805 esc_html__( 'Progressive lockout enabled (max: %d hours).', 'vigilante' ),
3806 absint( ceil( $max_lockout / 3600 ) )
3807 );
3808 }
3809
3810 if ( $two_factor_enabled ) {
3811 echo '<br>';
3812 esc_html_e( 'Failed 2FA codes also count toward the lockout limit.', 'vigilante' );
3813 }
3814 ?>
3815 </td>
3816 </tr>
3817 <tr>
3818 <th scope="row"><?php esc_html_e( 'Blocked IPs', 'vigilante' ); ?></th>
3819 <td>
3820 <?php if ( ! empty( $lockouts ) ) : ?>
3821 <div class="vigilante-paginated-section">
3822 <div class="vigilante-fi-pagination-wrap"></div>
3823 <table class="wp-list-table widefat fixed striped vigilante-fi-paginated">
3824 <thead>
3825 <tr>
3826 <th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th>
3827 <th><?php esc_html_e( 'Attempts', 'vigilante' ); ?></th>
3828 <th><?php esc_html_e( 'Blocked Until', 'vigilante' ); ?></th>
3829 <th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
3830 </tr>
3831 </thead>
3832 <tbody>
3833 <?php foreach ( $lockouts as $lockout ) :
3834 $lockout_time = strtotime( $lockout->locked_until );
3835 $remaining_seconds = $lockout_time - time();
3836 $remaining_text = $this->format_remaining_time( $remaining_seconds );
3837 ?>
3838 <tr>
3839 <td><code><?php echo esc_html( $lockout->ip_address ); ?></code></td>
3840 <td><?php echo esc_html( $lockout->attempts ); ?></td>
3841 <td>
3842 <?php echo esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $lockout_time ) ); ?>
3843 <br>
3844 <small class="description">
3845 <?php
3846 printf(
3847 /* translators: %s: Remaining time */
3848 esc_html__( '%s remaining', 'vigilante' ),
3849 esc_html( $remaining_text )
3850 );
3851 ?>
3852 </small>
3853 </td>
3854 <td>
3855 <button type="button" class="button button-small vigilante-clear-lockout" data-ip="<?php echo esc_attr( $lockout->ip_address ); ?>">
3856 <?php esc_html_e( 'Unblock', 'vigilante' ); ?>
3857 </button>
3858 </td>
3859 </tr>
3860 <?php endforeach; ?>
3861 </tbody>
3862 </table>
3863 </div>
3864 <p class="description" style="margin-top: 10px;">
3865 <button type="button" class="button vigilante-clear-all-lockouts">
3866 <?php esc_html_e( 'Unblock All IPs', 'vigilante' ); ?>
3867 </button>
3868 <span style="margin-left: 10px;">
3869 <?php
3870 printf(
3871 /* translators: %d: Number of blocked IPs */
3872 esc_html( _n( '%d IP currently blocked', '%d IPs currently blocked', count( $lockouts ), 'vigilante' ) ),
3873 count( $lockouts )
3874 );
3875 ?>
3876 </span>
3877 </p>
3878 <?php else : ?>
3879 <span class="dashicons dashicons-yes-alt" style="color: #00a32a; font-size: 20px; width: 20px; height: 20px; vertical-align: middle;"></span>
3880 <span style="vertical-align: middle; margin-left: 5px;"><?php esc_html_e( 'No IPs are currently blocked. All clear!', 'vigilante' ); ?></span>
3881 <?php endif; ?>
3882 </td>
3883 </tr>
3884 </table>
3885 </div>
3886 <?php
3887 }
3888
3889 /**
3890 * Format remaining lockout time in human readable format
3891 *
3892 * @param int $seconds Remaining seconds.
3893 * @return string Formatted time string.
3894 */
3895 private function format_remaining_time( $seconds ) {
3896 if ( $seconds <= 0 ) {
3897 return __( 'expired', 'vigilante' );
3898 }
3899
3900 if ( $seconds < 60 ) {
3901 return sprintf(
3902 /* translators: %d: Number of seconds */
3903 _n( '%d second', '%d seconds', $seconds, 'vigilante' ),
3904 $seconds
3905 );
3906 }
3907
3908 if ( $seconds < 3600 ) {
3909 $minutes = ceil( $seconds / 60 );
3910 return sprintf(
3911 /* translators: %d: Number of minutes */
3912 _n( '%d minute', '%d minutes', $minutes, 'vigilante' ),
3913 $minutes
3914 );
3915 }
3916
3917 $hours = floor( $seconds / 3600 );
3918 $remaining_minutes = ceil( ( $seconds % 3600 ) / 60 );
3919
3920 if ( $remaining_minutes > 0 ) {
3921 return sprintf(
3922 /* translators: 1: Number of hours, 2: Number of minutes */
3923 __( '%1$d hours %2$d minutes', 'vigilante' ),
3924 $hours,
3925 $remaining_minutes
3926 );
3927 }
3928
3929 return sprintf(
3930 /* translators: %d: Number of hours */
3931 _n( '%d hour', '%d hours', $hours, 'vigilante' ),
3932 $hours
3933 );
3934 }
3935
3936 /**
3937 * Render 2FA settings section
3938 *
3939 * @param array $options Login security options.
3940 */
3941 private function render_2fa_settings( $options ) {
3942 $two_factor = $options['two_factor'] ?? array();
3943 $all_roles = wp_roles()->roles;
3944 $enforced = $two_factor['enforced_roles'] ?? array( 'administrator', 'editor' );
3945 $excluded = $two_factor['excluded_users'] ?? array();
3946 $method = $two_factor['method'] ?? 'email';
3947 $grace_days = $two_factor['grace_period_days'] ?? 3;
3948 ?>
3949 <h3 id="vigilante-section-login-2fa">
3950 <?php esc_html_e( 'Two-Factor Authentication (2FA)', 'vigilante' ); ?>
3951 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
3952 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
3953 </h3>
3954 <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>
3955
3956 <table class="form-table">
3957 <tr>
3958 <th scope="row"><?php esc_html_e( 'Enable 2FA', 'vigilante' ); ?></th>
3959 <td>
3960 <label>
3961 <input type="checkbox" name="login_security[two_factor][enabled]" id="vigilante_2fa_enabled" value="1" <?php checked( ! empty( $two_factor['enabled'] ) ); ?>>
3962 <?php esc_html_e( 'Enable two-factor authentication', 'vigilante' ); ?>
3963 </label>
3964 </td>
3965 </tr>
3966 </table>
3967
3968 <div class="vigilante-2fa-settings-wrapper <?php echo empty( $two_factor['enabled'] ) ? 'vigilante-2fa-settings-disabled' : ''; ?>">
3969 <table class="form-table">
3970 <tr>
3971 <th scope="row"><?php esc_html_e( 'Verification method', 'vigilante' ); ?></th>
3972 <td>
3973 <fieldset class="vigilante-2fa-method-selector">
3974 <label class="vigilante-2fa-method-option <?php echo 'email' === $method ? 'selected' : ''; ?>">
3975 <input type="radio" name="login_security[two_factor][method]" value="email" <?php checked( $method, 'email' ); ?>>
3976 <span class="dashicons dashicons-email"></span>
3977 <span class="method-info">
3978 <strong><?php esc_html_e( 'Email code', 'vigilante' ); ?></strong>
3979 <span><?php esc_html_e( 'Users receive a 6-digit code via email after entering their password.', 'vigilante' ); ?></span>
3980 </span>
3981 </label>
3982 <label class="vigilante-2fa-method-option <?php echo 'totp' === $method ? 'selected' : ''; ?>">
3983 <input type="radio" name="login_security[two_factor][method]" value="totp" <?php checked( $method, 'totp' ); ?>>
3984 <span class="dashicons dashicons-smartphone"></span>
3985 <span class="method-info">
3986 <strong><?php esc_html_e( 'Authenticator app (TOTP)', 'vigilante' ); ?></strong>
3987 <span><?php esc_html_e( 'Users verify with a time-based code from Google Authenticator, Authy, etc.', 'vigilante' ); ?></span>
3988 </span>
3989 </label>
3990 </fieldset>
3991 </td>
3992 </tr>
3993 <tr>
3994 <th scope="row"><?php esc_html_e( 'Enforce for roles', 'vigilante' ); ?></th>
3995 <td>
3996 <div class="vigilante-2fa-roles">
3997 <?php foreach ( $all_roles as $role_slug => $role_data ) :
3998 $user_count = count( get_users( array( 'role' => $role_slug, 'fields' => 'ID' ) ) );
3999 ?>
4000 <label>
4001 <input type="checkbox"
4002 name="login_security[two_factor][enforced_roles][]"
4003 value="<?php echo esc_attr( $role_slug ); ?>"
4004 <?php checked( in_array( $role_slug, $enforced, true ) ); ?>>
4005 <span class="role-name"><?php echo esc_html( translate_user_role( $role_data['name'] ) ); ?></span>
4006 <span class="role-count">(<?php echo esc_html( $user_count ); ?>)</span>
4007 </label>
4008 <?php endforeach; ?>
4009 </div>
4010 <p class="description"><?php esc_html_e( 'Users with these roles will be required to verify with the selected method.', 'vigilante' ); ?></p>
4011 </td>
4012 </tr>
4013 <tr>
4014 <th scope="row"><?php esc_html_e( 'Exclude specific users', 'vigilante' ); ?></th>
4015 <td>
4016 <div class="vigilante-2fa-user-search-container">
4017 <div class="vigilante-2fa-user-search">
4018 <span class="search-icon"></span>
4019 <input type="text"
4020 id="vigilante_2fa_user_search"
4021 placeholder="<?php esc_attr_e( 'Search users by name or email...', 'vigilante' ); ?>"
4022 autocomplete="off">
4023 <div class="vigilante-2fa-search-results"></div>
4024 </div>
4025 <div class="vigilante-2fa-excluded-users">
4026 <?php
4027 foreach ( $excluded as $user_id ) :
4028 $user = get_user_by( 'ID', $user_id );
4029 if ( ! $user ) continue;
4030 ?>
4031 <div class="vigilante-2fa-excluded-user" data-user-id="<?php echo esc_attr( $user_id ); ?>">
4032 <span class="user-display"><?php echo esc_html( $user->display_name . ' (' . $user->user_email . ')' ); ?></span>
4033 <button type="button" class="remove-user" aria-label="<?php esc_attr_e( 'Remove', 'vigilante' ); ?>">&times;</button>
4034 <input type="hidden" name="login_security[two_factor][excluded_users][]" value="<?php echo esc_attr( $user_id ); ?>">
4035 </div>
4036 <?php endforeach; ?>
4037 </div>
4038 </div>
4039 <p class="description"><?php esc_html_e( 'These users will not be required to use 2FA regardless of their role.', 'vigilante' ); ?></p>
4040 </td>
4041 </tr>
4042
4043 <!-- Remember device option -->
4044 <tr>
4045 <th scope="row"><?php esc_html_e( 'Remember device', 'vigilante' ); ?></th>
4046 <td>
4047 <label>
4048 <input type="checkbox" name="login_security[two_factor][allow_remember_device]" value="1" <?php checked( ! empty( $two_factor['allow_remember_device'] ) ); ?>>
4049 <?php esc_html_e( 'Allow users to skip 2FA verification on trusted devices', 'vigilante' ); ?>
4050 </label>
4051 <p class="description">
4052 <?php
4053 printf(
4054 /* translators: %d: Number of days */
4055 esc_html__( 'When enabled, users can check "Remember this device" on the verification screen to skip 2FA for %d days.', 'vigilante' ),
4056 absint( $two_factor['remember_device_days'] ?? 30 )
4057 );
4058 ?>
4059 </p>
4060 </td>
4061 </tr>
4062
4063 <!-- TOTP-specific: Grace period -->
4064 <tr class="vigilante-2fa-totp-only" <?php echo 'totp' !== $method ? 'style="display:none;"' : ''; ?>>
4065 <th scope="row"><label for="vigilante-f-login-security-two-factor-grace-period-days"><?php esc_html_e( 'Grace period', 'vigilante' ); ?></label></th>
4066 <td>
4067 <input id="vigilante-f-login-security-two-factor-grace-period-days" type="number"
4068 name="login_security[two_factor][grace_period_days]"
4069 value="<?php echo esc_attr( $grace_days ); ?>"
4070 min="0" max="30" class="small-text">
4071 <?php esc_html_e( 'days', 'vigilante' ); ?>
4072 <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>
4073 </td>
4074 </tr>
4075
4076 <!-- Email-specific: Sender name -->
4077 <tr class="vigilante-2fa-email-only" <?php echo 'email' !== $method ? 'style="display:none;"' : ''; ?>>
4078 <th scope="row"><label for="vigilante-f-login-security-two-factor-email-from-name"><?php esc_html_e( 'Email sender name', 'vigilante' ); ?></label></th>
4079 <td>
4080 <input id="vigilante-f-login-security-two-factor-email-from-name" type="text"
4081 name="login_security[two_factor][email_from_name]"
4082 value="<?php echo esc_attr( $two_factor['email_from_name'] ?? '' ); ?>"
4083 class="regular-text vigilante-2fa-email-from"
4084 placeholder="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?>">
4085 <p class="description"><?php esc_html_e( 'Name shown in verification emails. Leave empty to use site name.', 'vigilante' ); ?></p>
4086 </td>
4087 </tr>
4088
4089 <!-- TOTP-specific: Reset users -->
4090 <tr class="vigilante-2fa-totp-only" <?php echo 'totp' !== $method ? 'style="display:none;"' : ''; ?>>
4091 <th scope="row"><?php esc_html_e( 'Reset user TOTP', 'vigilante' ); ?></th>
4092 <td>
4093 <div class="vigilante-totp-reset-container">
4094 <div class="vigilante-2fa-user-search">
4095 <span class="search-icon"></span>
4096 <input type="text"
4097 id="vigilante_totp_reset_search"
4098 placeholder="<?php esc_attr_e( 'Search users with TOTP configured...', 'vigilante' ); ?>"
4099 autocomplete="off">
4100 <div class="vigilante-totp-reset-results"></div>
4101 </div>
4102 <div class="vigilante-totp-reset-selected"></div>
4103 <button type="button" id="vigilante_totp_reset_btn" class="button" style="display:none;">
4104 <?php esc_html_e( 'Reset selected', 'vigilante' ); ?>
4105 </button>
4106 <span class="vigilante-totp-reset-status"></span>
4107 </div>
4108 <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>
4109 </td>
4110 </tr>
4111 </table>
4112
4113 <h3><?php esc_html_e( 'User notification', 'vigilante' ); ?></h3>
4114 <table class="form-table">
4115 <tr>
4116 <th scope="row"><?php esc_html_e( 'Notify on enable', 'vigilante' ); ?></th>
4117 <td>
4118 <label>
4119 <input type="checkbox" name="login_security[two_factor][notify_on_enable]" value="1" <?php checked( $two_factor['notify_on_enable'] ?? true ); ?>>
4120 <?php esc_html_e( 'Send notification email to affected users when 2FA is enabled', 'vigilante' ); ?>
4121 </label>
4122
4123 <div class="vigilante-2fa-notification-options">
4124 <label>
4125 <input type="radio" name="vigilante_2fa_notify_mode" value="all" checked>
4126 <?php esc_html_e( 'Send to all affected users', 'vigilante' ); ?>
4127 </label>
4128 <label>
4129 <input type="radio" name="vigilante_2fa_notify_mode" value="new">
4130 <?php esc_html_e( 'Send only to users not previously notified', 'vigilante' ); ?>
4131 </label>
4132 </div>
4133
4134 <div class="vigilante-2fa-send-notification">
4135 <button type="button" id="vigilante_2fa_send_notification" class="button">
4136 <?php esc_html_e( 'Send notification now', 'vigilante' ); ?>
4137 </button>
4138 <span class="vigilante-2fa-notification-status"></span>
4139 </div>
4140 </td>
4141 </tr>
4142 </table>
4143 </div>
4144 <?php
4145 }
4146
4147 /**
4148 * Render security headers tab
4149 */
4150 /**
4151 * Offer back the header settings the 2.9.8 migration wiped.
4152 *
4153 * Rendered outside the settings form on purpose, so its buttons can never
4154 * submit it, and only when there is something to actually change. Shows the
4155 * difference before anything is written: nothing is applied that the owner
4156 * has not seen first.
4157 *
4158 * @since 2.10.0
4159 */
4160 private function render_headers_recovery_offer() {
4161 /*
4162 * On a network the .htaccess belongs to every site and only the main one
4163 * writes it, so this is not a decision a subsite gets to make. Its own
4164 * security_headers options are inert anyway: what the network serves
4165 * comes from the file the main site owns. Without this gate a subsite
4166 * administrator was shown a Restore button that could only ever answer
4167 * with a permission error, which is worse than showing nothing.
4168 */
4169 if ( ! Vigilante_Settings::can_write_shared_files() ) {
4170 return;
4171 }
4172
4173 if ( ! Vigilante_Htaccess_Recovery::is_available() ) {
4174 /*
4175 * Already restored. Offer to take it back for as long as the previous
4176 * section is still stored: a restore that cannot be undone is a second
4177 * irreversible change on top of the one being repaired.
4178 */
4179 if ( Vigilante_Htaccess_Recovery::has_undo() ) {
4180 ?>
4181 <div class="notice notice-info inline" id="vigilante-headers-recovery-undo">
4182 <p>
4183 <?php esc_html_e( 'The Security Headers settings were restored from the copy Vigilant had kept of your .htaccess.', 'vigilante' ); ?>
4184 <button type="button" class="button button-small" id="vigilante-recovery-undo">
4185 <?php esc_html_e( 'Undo the restore', 'vigilante' ); ?>
4186 </button>
4187 </p>
4188 </div>
4189 <?php
4190 }
4191
4192 return;
4193 }
4194
4195 $rows = Vigilante_Htaccess_Recovery::get_diff( $this->settings );
4196
4197 if ( empty( $rows ) ) {
4198 return;
4199 }
4200
4201 $snapshot = Vigilante_Htaccess_Recovery::get_snapshot();
4202 $taken = isset( $snapshot['time'] ) ? (int) $snapshot['time'] : 0;
4203 $block = Vigilante_Htaccess_Recovery::get_raw_block();
4204 ?>
4205 <div class="vigilante-settings-section" id="vigilante-headers-recovery">
4206 <h2><?php esc_html_e( 'Recover your previous header settings', 'vigilante' ); ?></h2>
4207 <p>
4208 <?php esc_html_e( 'Updating to 2.9.8 reset this tab to factory values: the migration replaced the whole section instead of merging into it. Your server kept sending the right headers, because the .htaccess had not been rewritten yet, so Vigilant saved a copy of that file before touching it. These are the settings it found in that copy.', 'vigilante' ); ?>
4209 </p>
4210 <?php if ( $taken ) : ?>
4211 <p class="description">
4212 <?php
4213 printf(
4214 /* translators: %s: date and time the .htaccess copy was taken. */
4215 esc_html__( 'Copy taken on %s.', 'vigilante' ),
4216 esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $taken ) )
4217 );
4218 ?>
4219 </p>
4220 <?php endif; ?>
4221
4222 <table class="widefat striped">
4223 <thead>
4224 <tr>
4225 <th scope="col"><?php esc_html_e( 'Setting', 'vigilante' ); ?></th>
4226 <th scope="col"><?php esc_html_e( 'Now', 'vigilante' ); ?></th>
4227 <th scope="col"><?php esc_html_e( 'Would be restored to', 'vigilante' ); ?></th>
4228 </tr>
4229 </thead>
4230 <tbody>
4231 <?php foreach ( $rows as $row ) : ?>
4232 <tr>
4233 <th scope="row"><?php echo esc_html( $row['label'] ); ?></th>
4234 <td><?php echo esc_html( $row['current'] ); ?></td>
4235 <td>
4236 <?php echo esc_html( $row['recovered'] ); ?>
4237 <?php if ( ! empty( $row['detail'] ) ) : ?>
4238 <br><span class="description"><?php echo esc_html( $row['detail'] ); ?></span>
4239 <?php endif; ?>
4240 </td>
4241 </tr>
4242 <?php endforeach; ?>
4243 </tbody>
4244 </table>
4245
4246 <p class="description">
4247 <?php esc_html_e( 'Only these settings are written. The .htaccess is then rebuilt from them, the same way saving this tab rebuilds it. The stored copy of the file is never written back, so nothing your host, your cache plugin or your CDN added to it is touched.', 'vigilante' ); ?>
4248 </p>
4249
4250 <?php if ( '' !== $block ) : ?>
4251 <details>
4252 <summary><?php esc_html_e( 'Show the saved .htaccess block', 'vigilante' ); ?></summary>
4253 <textarea readonly rows="12" class="large-text code" onclick="this.select();"><?php echo esc_textarea( $block ); ?></textarea>
4254 </details>
4255 <?php endif; ?>
4256
4257 <p class="submit vigilante-submit-buttons">
4258 <button type="button" class="button button-primary" id="vigilante-recovery-restore">
4259 <?php esc_html_e( 'Restore these settings', 'vigilante' ); ?>
4260 </button>
4261 <button type="button" class="button" id="vigilante-recovery-dismiss">
4262 <?php esc_html_e( 'No thanks, keep what I have', 'vigilante' ); ?>
4263 </button>
4264 </p>
4265 <div id="vigilante-recovery-result"></div>
4266 </div>
4267 <?php
4268 }
4269
4270 private function render_tab_headers() {
4271 $is_disabled = $this->render_module_disabled_notice( 'security_headers' );
4272 // Every setting on this tab ends up in .htaccess, so on a subsite the
4273 // whole tab is somebody else's, values included.
4274 $vg_shared_locked = $this->shared_files_locked();
4275 $options = $this->get_section_for_display( 'security_headers' );
4276 ?>
4277 <?php $this->render_headers_recovery_offer(); ?>
4278
4279 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="security_headers" <?php echo $is_disabled ? 'inert' : ''; ?>>
4280 <?php $this->render_shared_files_notice(); ?>
4281 <div id="vigilante-section-headers-main" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
4282 <h2>
4283 <?php esc_html_e( 'Security Headers', 'vigilante' ); ?>
4284 <span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span>
4285 </h2>
4286 <p><?php esc_html_e( 'HTTP headers sent with every response via .htaccess (mod_headers).', 'vigilante' ); ?></p>
4287
4288 <table class="form-table">
4289 <tr>
4290 <th scope="row"><label for="vigilante-f-security-headers-x-frame-options"><?php esc_html_e( 'X-Frame-Options', 'vigilante' ); ?></label></th>
4291 <td>
4292 <select id="vigilante-f-security-headers-x-frame-options" name="security_headers[x_frame_options]">
4293 <option value="" <?php selected( empty( $options['x_frame_options'] ) ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option>
4294 <option value="SAMEORIGIN" <?php selected( $options['x_frame_options'] ?? '', 'SAMEORIGIN' ); ?>>SAMEORIGIN</option>
4295 <option value="DENY" <?php selected( $options['x_frame_options'] ?? '', 'DENY' ); ?>>DENY</option>
4296 </select>
4297 <p class="description"><?php esc_html_e( '&#9432; Prevents clickjacking attacks. Also sets CSP frame-ancestors automatically.', 'vigilante' ); ?></p>
4298 </td>
4299 </tr>
4300 <tr>
4301 <th scope="row"><?php esc_html_e( 'X-Content-Type-Options', 'vigilante' ); ?></th>
4302 <td>
4303 <label>
4304 <input type="checkbox" name="security_headers[x_content_type_options]" value="1" <?php checked( ! empty( $options['x_content_type_options'] ) ); ?>>
4305 <?php esc_html_e( 'Add nosniff header to prevent MIME type sniffing', 'vigilante' ); ?>
4306 </label>
4307 </td>
4308 </tr>
4309 <tr>
4310 <th scope="row"><label for="vigilante-f-security-headers-referrer-policy"><?php esc_html_e( 'Referrer-Policy', 'vigilante' ); ?></label></th>
4311 <td>
4312 <select id="vigilante-f-security-headers-referrer-policy" name="security_headers[referrer_policy]">
4313 <option value="" <?php selected( empty( $options['referrer_policy'] ) ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option>
4314 <option value="no-referrer" <?php selected( $options['referrer_policy'] ?? '', 'no-referrer' ); ?>>no-referrer</option>
4315 <option value="strict-origin-when-cross-origin" <?php selected( $options['referrer_policy'] ?? '', 'strict-origin-when-cross-origin' ); ?>>strict-origin-when-cross-origin</option>
4316 <option value="same-origin" <?php selected( $options['referrer_policy'] ?? '', 'same-origin' ); ?>>same-origin</option>
4317 </select>
4318 </td>
4319 </tr>
4320 </table>
4321
4322 <h3 id="vigilante-section-headers-csp"><?php esc_html_e( 'Content Security Policy', 'vigilante' ); ?></h3>
4323 <table class="form-table">
4324 <tr>
4325 <th scope="row"><?php esc_html_e( 'Enable CSP', 'vigilante' ); ?></th>
4326 <td>
4327 <label>
4328 <input type="checkbox" name="security_headers[csp][enabled]" value="1" <?php checked( ! empty( $options['csp']['enabled'] ) ); ?>>
4329 <?php esc_html_e( 'Enable Content Security Policy', 'vigilante' ); ?>
4330 </label>
4331 </td>
4332 </tr>
4333 <tr>
4334 <th scope="row"><?php esc_html_e( 'Report Only Mode', 'vigilante' ); ?></th>
4335 <td>
4336 <label>
4337 <input type="checkbox" name="security_headers[csp][report_only]" value="1" <?php checked( ! empty( $options['csp']['report_only'] ) ); ?>>
4338 <?php esc_html_e( 'Report violations without blocking (for testing)', 'vigilante' ); ?>
4339 </label>
4340 </td>
4341 </tr>
4342 </table>
4343
4344 <h3 id="vigilante-section-headers-force-https"><?php esc_html_e( 'HTTPS', 'vigilante' ); ?></h3>
4345 <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>
4346 <table class="form-table">
4347 <tr>
4348 <th scope="row"><?php esc_html_e( 'Redirect HTTP to HTTPS', 'vigilante' ); ?></th>
4349 <td>
4350 <label>
4351 <input type="checkbox" name="security_headers[redirect_http_to_https]" value="1" <?php checked( ! empty( $options['redirect_http_to_https'] ) ); ?>>
4352 <?php esc_html_e( 'Send visitors arriving over HTTP to the HTTPS address', 'vigilante' ); ?>
4353 </label>
4354 <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>
4355 </td>
4356 </tr>
4357 <tr>
4358 <th scope="row"><?php esc_html_e( 'Fix Mixed Content', 'vigilante' ); ?></th>
4359 <td>
4360 <label>
4361 <input type="checkbox" name="security_headers[fix_mixed_content]" value="1" <?php checked( ! empty( $options['fix_mixed_content'] ) ); ?>>
4362 <?php esc_html_e( 'Rewrite this site http:// resources to https://', 'vigilante' ); ?>
4363 </label>
4364 <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>
4365 </td>
4366 </tr>
4367 <tr id="field-upgrade-insecure-requests">
4368 <th scope="row"><?php esc_html_e( 'Upgrade Insecure Requests', 'vigilante' ); ?></th>
4369 <td>
4370 <label>
4371 <input type="checkbox" name="security_headers[upgrade_insecure_requests]" value="1" <?php checked( ! empty( $options['upgrade_insecure_requests'] ) ); ?>>
4372 <?php esc_html_e( 'Ask browsers to upgrade every http:// request to https://', 'vigilante' ); ?>
4373 </label>
4374 <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>
4375 </td>
4376 </tr>
4377 <tr>
4378 <th scope="row"><?php esc_html_e( 'Rewrite Site Address on Activation', 'vigilante' ); ?></th>
4379 <td>
4380 <label>
4381 <input type="checkbox" name="security_headers[force_https]" value="1" <?php checked( ! empty( $options['force_https'] ) ); ?>>
4382 <?php esc_html_e( 'Change the WordPress and site addresses to https:// when the plugin is activated', 'vigilante' ); ?>
4383 </label>
4384 <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>
4385 </td>
4386 </tr>
4387 </table>
4388
4389 <h3 id="vigilante-section-headers-hsts"><?php esc_html_e( 'HSTS (HTTP Strict Transport Security)', 'vigilante' ); ?></h3>
4390 <?php $vig_home_https = ( 0 === strpos( (string) get_option( 'home' ), 'https://' ) ); ?>
4391 <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>
4392 <?php if ( ! $vig_home_https ) : ?>
4393 <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>
4394 <?php endif; ?>
4395 <table class="form-table">
4396 <tr>
4397 <th scope="row"><?php esc_html_e( 'Enable HSTS', 'vigilante' ); ?></th>
4398 <td>
4399 <?php if ( ! $vig_home_https ) : ?>
4400 <?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. */ ?>
4401 <input type="hidden" name="security_headers[hsts][enabled]" value="<?php echo ! empty( $options['hsts']['enabled'] ) ? '1' : '0'; ?>">
4402 <?php endif; ?>
4403 <label>
4404 <input type="checkbox" name="security_headers[hsts][enabled]" value="1" <?php checked( ! empty( $options['hsts']['enabled'] ) ); ?> <?php disabled( ! $vig_home_https ); ?>>
4405 <?php esc_html_e( 'Send the Strict-Transport-Security header', 'vigilante' ); ?>
4406 </label>
4407 <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>
4408 </td>
4409 </tr>
4410 <tr>
4411 <th scope="row"><label for="vigilante-f-security-headers-hsts-max-age"><?php esc_html_e( 'Max Age', 'vigilante' ); ?></label></th>
4412 <td>
4413 <select id="vigilante-f-security-headers-hsts-max-age" name="security_headers[hsts][max_age]">
4414 <option value="86400" <?php selected( $options['hsts']['max_age'] ?? 31536000, 86400 ); ?>><?php esc_html_e( '1 day (testing)', 'vigilante' ); ?></option>
4415 <option value="2592000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 2592000 ); ?>><?php esc_html_e( '30 days', 'vigilante' ); ?></option>
4416 <option value="31536000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 31536000 ); ?>><?php esc_html_e( '1 year (recommended)', 'vigilante' ); ?></option>
4417 <option value="63072000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 63072000 ); ?>><?php esc_html_e( '2 years', 'vigilante' ); ?></option>
4418 </select>
4419 </td>
4420 </tr>
4421 <tr>
4422 <th scope="row"><?php esc_html_e( 'Include Subdomains', 'vigilante' ); ?></th>
4423 <td>
4424 <label>
4425 <input type="checkbox" name="security_headers[hsts][include_subdomains]" value="1" <?php checked( ! empty( $options['hsts']['include_subdomains'] ) ); ?>>
4426 <?php esc_html_e( 'Apply HSTS to all subdomains', 'vigilante' ); ?>
4427 </label>
4428 </td>
4429 </tr>
4430 </table>
4431
4432 <h3 id="vigilante-section-headers-fingerprint"><?php esc_html_e( 'Server Identity', 'vigilante' ); ?></h3>
4433 <p class="description"><?php esc_html_e( 'Hide identifying information that servers expose in responses.', 'vigilante' ); ?></p>
4434 <table class="form-table">
4435 <tr>
4436 <th scope="row"><?php esc_html_e( 'Server Signature', 'vigilante' ); ?></th>
4437 <td>
4438 <label>
4439 <input type="checkbox" name="security_headers[hide_server_signature]" value="1" <?php checked( ! empty( $options['hide_server_signature'] ) ); ?>>
4440 <?php esc_html_e( 'Hide server signature (ServerSignature Off)', 'vigilante' ); ?>
4441 </label>
4442 </td>
4443 </tr>
4444 <tr>
4445 <th scope="row"><?php esc_html_e( 'Remove Fingerprinting Headers', 'vigilante' ); ?></th>
4446 <td>
4447 <label>
4448 <input type="checkbox" name="security_headers[remove_fingerprinting_headers]" value="1" <?php checked( ! empty( $options['remove_fingerprinting_headers'] ) ); ?>>
4449 <?php esc_html_e( 'Remove X-Powered-By and Server headers', 'vigilante' ); ?>
4450 </label>
4451 </td>
4452 </tr>
4453 </table>
4454 </div>
4455
4456 <?php $vg_cop = ( isset( $options['cross_origin_policies'] ) && is_array( $options['cross_origin_policies'] ) ) ? $options['cross_origin_policies'] : array(); ?>
4457 <div id="vigilante-section-headers-cross-origin" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
4458 <h2>
4459 <?php esc_html_e( 'Cross-Origin Policies', 'vigilante' ); ?>
4460 <span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span>
4461 </h2>
4462 <p><?php esc_html_e( 'Control how other origins may open, embed or fetch your site. Vigilant already sends these headers with the values below.', 'vigilante' ); ?></p>
4463
4464 <table class="form-table">
4465 <tr>
4466 <th scope="row"><label for="vigilante-f-security-headers-coop"><?php esc_html_e( 'Cross-Origin-Opener-Policy (COOP)', 'vigilante' ); ?></label></th>
4467 <td>
4468 <select id="vigilante-f-security-headers-coop" name="security_headers[cross_origin_policies][opener_policy]">
4469 <option value="" <?php selected( empty( $vg_cop['opener_policy'] ) ); ?>><?php esc_html_e( 'Disabled (header not sent)', 'vigilante' ); ?></option>
4470 <option value="unsafe-none" <?php selected( $vg_cop['opener_policy'] ?? '', 'unsafe-none' ); ?>>unsafe-none</option>
4471 <option value="same-origin-allow-popups" <?php selected( $vg_cop['opener_policy'] ?? '', 'same-origin-allow-popups' ); ?>><?php esc_html_e( 'same-origin-allow-popups (recommended)', 'vigilante' ); ?></option>
4472 <option value="same-origin" <?php selected( $vg_cop['opener_policy'] ?? '', 'same-origin' ); ?>>same-origin</option>
4473 </select>
4474 <p class="description"><?php esc_html_e( '&#9432; Cuts the link between your site and a window from another origin that opened it. Side effect: external tools that open your site in a new tab and talk to it through window.opener, such as Google Tag Assistant, will report that they cannot connect. Pick unsafe-none or Disabled if you need those tools.', 'vigilante' ); ?></p>
4475 </td>
4476 </tr>
4477 <tr>
4478 <th scope="row"><label for="vigilante-f-security-headers-coep"><?php esc_html_e( 'Cross-Origin-Embedder-Policy (COEP)', 'vigilante' ); ?></label></th>
4479 <td>
4480 <select id="vigilante-f-security-headers-coep" name="security_headers[cross_origin_policies][embedder_policy]">
4481 <option value="unsafe-none" <?php selected( ( $vg_cop['embedder_policy'] ?? 'unsafe-none' ), 'unsafe-none' ); ?>><?php esc_html_e( 'unsafe-none (header not sent)', 'vigilante' ); ?></option>
4482 <option value="credentialless" <?php selected( $vg_cop['embedder_policy'] ?? '', 'credentialless' ); ?>>credentialless</option>
4483 <option value="require-corp" <?php selected( $vg_cop['embedder_policy'] ?? '', 'require-corp' ); ?>>require-corp</option>
4484 </select>
4485 <p class="description"><?php esc_html_e( '&#9432; Requires every cross-origin resource to opt in. require-corp can block third-party images, fonts, videos and embeds that do not send their own CORP or CORS headers.', 'vigilante' ); ?></p>
4486 </td>
4487 </tr>
4488 <tr>
4489 <th scope="row"><label for="vigilante-f-security-headers-corp"><?php esc_html_e( 'Cross-Origin-Resource-Policy (CORP)', 'vigilante' ); ?></label></th>
4490 <td>
4491 <select id="vigilante-f-security-headers-corp" name="security_headers[cross_origin_policies][resource_policy]">
4492 <option value="" <?php selected( empty( $vg_cop['resource_policy'] ) ); ?>><?php esc_html_e( 'Disabled (header not sent)', 'vigilante' ); ?></option>
4493 <option value="same-site" <?php selected( $vg_cop['resource_policy'] ?? '', 'same-site' ); ?>>same-site</option>
4494 <option value="same-origin" <?php selected( $vg_cop['resource_policy'] ?? '', 'same-origin' ); ?>>same-origin</option>
4495 <option value="cross-origin" <?php selected( $vg_cop['resource_policy'] ?? '', 'cross-origin' ); ?>><?php esc_html_e( 'cross-origin (recommended)', 'vigilante' ); ?></option>
4496 </select>
4497 <p class="description"><?php esc_html_e( '&#9432; Declares who may load resources from this site. same-origin stops hotlinking, but it also breaks CDNs, feed readers and any external service that fetches your images or files.', 'vigilante' ); ?></p>
4498 </td>
4499 </tr>
4500 </table>
4501 </div>
4502
4503 <p class="submit vigilante-submit-buttons">
4504 <?php if ( ! $vg_shared_locked ) : ?>
4505 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
4506 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
4507 </button>
4508 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
4509 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
4510 </button>
4511 <?php endif; ?>
4512 <?php /* Testing what the server actually sends is read-only and useful from any site of a network. */ ?>
4513 <button type="button" class="button vigilante-test-headers">
4514 <?php esc_html_e( 'Test Headers', 'vigilante' ); ?>
4515 </button>
4516 </p>
4517 </form>
4518
4519 <div id="vigilante-headers-result"></div>
4520 <?php
4521 }
4522
4523 /**
4524 * Render REST API tab
4525 */
4526 private function render_tab_rest_api() {
4527 $is_disabled = $this->render_module_disabled_notice( 'rest_api_security' );
4528 $options = $this->settings->get_section( 'rest_api_security' );
4529 ?>
4530 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="rest_api_security" <?php echo $is_disabled ? 'inert' : ''; ?>>
4531 <div id="vigilante-section-rest-api-main" class="vigilante-settings-section">
4532 <h2>
4533 <?php esc_html_e( 'REST API Security', 'vigilante' ); ?>
4534 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4535 </h2>
4536 <p><?php esc_html_e( 'Control access to WordPress REST API endpoints.', 'vigilante' ); ?></p>
4537
4538 <table class="form-table">
4539 <tr>
4540 <th scope="row"><label for="vigilante-f-rest-api-security-mode"><?php esc_html_e( 'Access Mode', 'vigilante' ); ?></label></th>
4541 <td>
4542 <select id="vigilante-f-rest-api-security-mode" name="rest_api_security[mode]">
4543 <option value="open" <?php selected( $options['mode'] ?? 'selective', 'open' ); ?>><?php esc_html_e( 'Open - Allow all requests', 'vigilante' ); ?></option>
4544 <option value="selective" <?php selected( $options['mode'] ?? 'selective', 'selective' ); ?>><?php esc_html_e( 'Selective - Protect sensitive endpoints', 'vigilante' ); ?></option>
4545 <option value="authenticated_only" <?php selected( $options['mode'] ?? 'selective', 'authenticated_only' ); ?>><?php esc_html_e( 'Authenticated - Require login for all', 'vigilante' ); ?></option>
4546 </select>
4547 <p class="description"><?php esc_html_e( 'Selective mode is recommended.', 'vigilante' ); ?></p>
4548 </td>
4549 </tr>
4550 <tr id="field-block-user-enumeration">
4551 <th scope="row"><?php esc_html_e( 'Block User Enumeration', 'vigilante' ); ?></th>
4552 <td>
4553 <label>
4554 <input type="checkbox" name="rest_api_security[block_user_enumeration]" value="1" <?php checked( ! empty( $options['block_user_enumeration'] ) ); ?>>
4555 <?php esc_html_e( 'Protect /wp/v2/users endpoint for unauthenticated users', 'vigilante' ); ?>
4556 </label>
4557 </td>
4558 </tr>
4559 <tr>
4560 <th scope="row"><?php esc_html_e( 'Disable JSONP', 'vigilante' ); ?></th>
4561 <td>
4562 <label>
4563 <input type="checkbox" name="rest_api_security[disable_jsonp]" value="1" <?php checked( ! empty( $options['disable_jsonp'] ) ); ?>>
4564 <?php esc_html_e( 'Disable JSONP support in REST API', 'vigilante' ); ?>
4565 </label>
4566 </td>
4567 </tr>
4568 </table>
4569 </div>
4570
4571 <p class="submit vigilante-submit-buttons">
4572 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
4573 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
4574 </button>
4575 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
4576 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
4577 </button>
4578 </p>
4579 </form>
4580 <?php
4581 }
4582
4583 /**
4584 * Render User Security tab
4585 */
4586 private function render_tab_users() {
4587 $is_disabled = $this->render_module_disabled_notice( 'user_security' );
4588 $options = $this->settings->get_section( 'user_security' );
4589 $monitoring = $options['admin_monitoring'] ?? array();
4590 $registration = $options['registration_approval'] ?? array();
4591 $session_limits = $options['session_limits'] ?? array();
4592 $password_exp = $options['password_expiration'] ?? array();
4593 $email_verify = $options['email_verification'] ?? array();
4594 ?>
4595
4596 <!-- ============================================================
4597 SETTINGS SECTION - Single form for all configuration
4598 ============================================================ -->
4599 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="user_security" <?php echo $is_disabled ? 'inert' : ''; ?>>
4600
4601 <!-- Username & Password Protection -->
4602 <div id="vigilante-section-users-password" class="vigilante-settings-section">
4603 <h2>
4604 <?php esc_html_e( 'Username & password protection', 'vigilante' ); ?>
4605 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4606 </h2>
4607 <p><?php esc_html_e( 'Enforce secure username and password policies.', 'vigilante' ); ?></p>
4608
4609 <table class="form-table">
4610 <tr>
4611 <th scope="row"><?php esc_html_e( 'Block Insecure Usernames', 'vigilante' ); ?></th>
4612 <td>
4613 <label>
4614 <input type="checkbox" name="user_security[block_insecure_usernames]" value="1" <?php checked( ! empty( $options['block_insecure_usernames'] ) ); ?>>
4615 <?php esc_html_e( 'Prevent creation of users with common usernames (admin, administrator, etc.)', 'vigilante' ); ?>
4616 </label>
4617 </td>
4618 </tr>
4619 <?php
4620 $pw_policy = wp_parse_args(
4621 ( isset( $options['password_policy'] ) && is_array( $options['password_policy'] ) ) ? $options['password_policy'] : array(),
4622 array(
4623 'require_uppercase' => false,
4624 'require_lowercase' => false,
4625 'require_number' => false,
4626 'require_special' => false,
4627 'block_common' => true,
4628 'block_username' => true,
4629 'affected_roles' => array(),
4630 )
4631 );
4632 $pw_policy_roles = (array) $pw_policy['affected_roles'];
4633 ?>
4634 <tr>
4635 <th scope="row"><?php esc_html_e( 'Enforce Strong Passwords', 'vigilante' ); ?></th>
4636 <td>
4637 <label>
4638 <input type="checkbox" name="user_security[force_strong_passwords]" value="1" <?php checked( ! empty( $options['force_strong_passwords'] ) ); ?>>
4639 <?php esc_html_e( 'Check passwords against the requirements below when a user sets or changes one', 'vigilante' ); ?>
4640 </label>
4641 </td>
4642 </tr>
4643 <tr>
4644 <th scope="row"><label for="vigilante-f-user-security-min-password-length"><?php esc_html_e( 'Minimum Password Length', 'vigilante' ); ?></label></th>
4645 <td>
4646 <input id="vigilante-f-user-security-min-password-length" 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">
4647 <?php esc_html_e( 'characters', 'vigilante' ); ?>
4648 </td>
4649 </tr>
4650 <tr>
4651 <th scope="row"><?php esc_html_e( 'Password Requirements', 'vigilante' ); ?></th>
4652 <td>
4653 <label style="display:block;margin-bottom:5px;">
4654 <input type="checkbox" name="user_security[password_policy][require_uppercase]" value="1" <?php checked( ! empty( $pw_policy['require_uppercase'] ) ); ?>>
4655 <?php esc_html_e( 'Require an uppercase letter (A-Z)', 'vigilante' ); ?>
4656 </label>
4657 <label style="display:block;margin-bottom:5px;">
4658 <input type="checkbox" name="user_security[password_policy][require_lowercase]" value="1" <?php checked( ! empty( $pw_policy['require_lowercase'] ) ); ?>>
4659 <?php esc_html_e( 'Require a lowercase letter (a-z)', 'vigilante' ); ?>
4660 </label>
4661 <label style="display:block;margin-bottom:5px;">
4662 <input type="checkbox" name="user_security[password_policy][require_number]" value="1" <?php checked( ! empty( $pw_policy['require_number'] ) ); ?>>
4663 <?php esc_html_e( 'Require a number (0-9)', 'vigilante' ); ?>
4664 </label>
4665 <label style="display:block;margin-bottom:5px;">
4666 <input type="checkbox" name="user_security[password_policy][require_special]" value="1" <?php checked( ! empty( $pw_policy['require_special'] ) ); ?>>
4667 <?php esc_html_e( 'Require a special character (!, @, #, ...)', 'vigilante' ); ?>
4668 </label>
4669 <label style="display:block;margin-bottom:5px;">
4670 <input type="checkbox" name="user_security[password_policy][block_common]" value="1" <?php checked( ! empty( $pw_policy['block_common'] ) ); ?>>
4671 <?php esc_html_e( 'Reject well-known common passwords', 'vigilante' ); ?>
4672 </label>
4673 <label style="display:block;margin-bottom:5px;">
4674 <input type="checkbox" name="user_security[password_policy][block_username]" value="1" <?php checked( ! empty( $pw_policy['block_username'] ) ); ?>>
4675 <?php esc_html_e( 'Do not allow the username inside the password', 'vigilante' ); ?>
4676 </label>
4677 <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>
4678 </td>
4679 </tr>
4680 <tr>
4681 <th scope="row"><?php esc_html_e( 'Apply Password Rules To', 'vigilante' ); ?></th>
4682 <td>
4683 <?php foreach ( wp_roles()->get_names() as $role_slug => $role_name ) : ?>
4684 <label style="display:block;margin-bottom:5px;">
4685 <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 ) ); ?>>
4686 <?php echo esc_html( translate_user_role( $role_name ) ); ?>
4687 </label>
4688 <?php endforeach; ?>
4689 <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>
4690 </td>
4691 </tr>
4692 <tr id="field-block-author-scanning">
4693 <th scope="row"><?php esc_html_e( 'Block Author Scanning', 'vigilante' ); ?></th>
4694 <td>
4695 <label>
4696 <input type="checkbox" name="user_security[block_author_scanning]" value="1" <?php checked( ! empty( $options['block_author_scanning'] ) ); ?>>
4697 <?php esc_html_e( 'Prevent username discovery via ?author=N URLs', 'vigilante' ); ?>
4698 </label>
4699 </td>
4700 </tr>
4701 <tr>
4702 <th scope="row"><?php esc_html_e( 'Display Name Protection', 'vigilante' ); ?></th>
4703 <td>
4704 <label>
4705 <input type="checkbox" name="user_security[prevent_display_name_login_match]" value="1" <?php checked( ! empty( $options['prevent_display_name_login_match'] ) ); ?>>
4706 <?php esc_html_e( 'Prevent users from saving a display name that matches their login username', 'vigilante' ); ?>
4707 </label>
4708 <p class="description"><?php esc_html_e( 'The display name is publicly visible and should not reveal the login username.', 'vigilante' ); ?></p>
4709 </td>
4710 </tr>
4711 </table>
4712 </div>
4713
4714 <!-- Admin Monitoring -->
4715 <div id="vigilante-section-users-admin-monitoring" class="vigilante-settings-section">
4716 <h2>
4717 <?php esc_html_e( 'Admin monitoring', 'vigilante' ); ?>
4718 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4719 </h2>
4720 <p><?php esc_html_e( 'Receive email alerts when administrator accounts are modified. All events are always logged to the Security Audit.', 'vigilante' ); ?></p>
4721
4722 <table class="form-table">
4723 <tr>
4724 <th scope="row"><?php esc_html_e( 'New Administrator Alert', 'vigilante' ); ?></th>
4725 <td>
4726 <label>
4727 <input type="checkbox" name="user_security[admin_monitoring][alert_new_admin]" value="1" <?php checked( ! empty( $monitoring['alert_new_admin'] ) ); ?>>
4728 <?php esc_html_e( 'Send email alert when a new administrator account is created', 'vigilante' ); ?>
4729 </label>
4730 </td>
4731 </tr>
4732 <tr>
4733 <th scope="row"><?php esc_html_e( 'Admin Email Change Alert', 'vigilante' ); ?></th>
4734 <td>
4735 <label>
4736 <input type="checkbox" name="user_security[admin_monitoring][alert_admin_email_change]" value="1" <?php checked( ! empty( $monitoring['alert_admin_email_change'] ) ); ?>>
4737 <?php esc_html_e( 'Send email alert when an administrator email address is changed', 'vigilante' ); ?>
4738 </label>
4739 </td>
4740 </tr>
4741 <tr>
4742 <th scope="row"><?php esc_html_e( 'Permission Elevation Alert', 'vigilante' ); ?></th>
4743 <td>
4744 <label>
4745 <input type="checkbox" name="user_security[admin_monitoring][alert_permission_elevation]" value="1" <?php checked( ! empty( $monitoring['alert_permission_elevation'] ) ); ?>>
4746 <?php esc_html_e( 'Send email alert when a user is elevated to administrator role', 'vigilante' ); ?>
4747 </label>
4748 </td>
4749 </tr>
4750 <tr>
4751 <th scope="row"><?php esc_html_e( 'Admin Password Change Alert', 'vigilante' ); ?></th>
4752 <td>
4753 <label>
4754 <input type="checkbox" name="user_security[admin_monitoring][alert_admin_password_change]" value="1" <?php checked( ! empty( $monitoring['alert_admin_password_change'] ) ); ?>>
4755 <?php esc_html_e( 'Send email alert when an administrator password is changed', 'vigilante' ); ?>
4756 </label>
4757 </td>
4758 </tr>
4759 </table>
4760
4761 <p class="description">
4762 <?php
4763 printf(
4764 /* translators: %s: Link to notification settings */
4765 esc_html__( '&#9432; Notifications are sent to the recipients configured in %s.', 'vigilante' ),
4766 '<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>'
4767 );
4768 ?>
4769 </p>
4770 </div>
4771
4772 <!-- Registration Approval -->
4773 <div id="vigilante-section-users-registration" class="vigilante-settings-section">
4774 <h2>
4775 <?php esc_html_e( 'Registration approval', 'vigilante' ); ?>
4776 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4777 </h2>
4778 <p><?php esc_html_e( 'Require manual approval for new user registrations.', 'vigilante' ); ?></p>
4779
4780 <table class="form-table">
4781 <tr>
4782 <th scope="row"><?php esc_html_e( 'Enable Registration Approval', 'vigilante' ); ?></th>
4783 <td>
4784 <label>
4785 <input type="checkbox" name="user_security[registration_approval][enabled]" value="1" <?php checked( ! empty( $registration['enabled'] ) ); ?>>
4786 <?php esc_html_e( 'New users must be approved by an administrator before they can log in', 'vigilante' ); ?>
4787 </label>
4788 </td>
4789 </tr>
4790 <tr>
4791 <th scope="row"><?php esc_html_e( 'Notify Admin', 'vigilante' ); ?></th>
4792 <td>
4793 <label>
4794 <input type="checkbox" name="user_security[registration_approval][notify_admin]" value="1" <?php checked( ! empty( $registration['notify_admin'] ) ); ?>>
4795 <?php esc_html_e( 'Send email notification when a new user registers', 'vigilante' ); ?>
4796 </label>
4797 <p class="description"><?php esc_html_e( 'Disable on high-traffic sites to avoid email overload.', 'vigilante' ); ?></p>
4798 </td>
4799 </tr>
4800 <tr>
4801 <th scope="row"><label for="vigilante-f-user-security-registration-approval-auto-reject-days"><?php esc_html_e( 'Auto-reject After', 'vigilante' ); ?></label></th>
4802 <td>
4803 <input id="vigilante-f-user-security-registration-approval-auto-reject-days" 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">
4804 <?php esc_html_e( 'days (0 = never)', 'vigilante' ); ?>
4805 <p class="description"><?php esc_html_e( 'Automatically reject pending registrations after this many days.', 'vigilante' ); ?></p>
4806 </td>
4807 </tr>
4808 </table>
4809 </div>
4810
4811 <!-- Session Limits -->
4812 <div id="vigilante-section-users-sessions" class="vigilante-settings-section">
4813 <h2>
4814 <?php esc_html_e( 'Session limits', 'vigilante' ); ?>
4815 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4816 </h2>
4817 <p><?php esc_html_e( 'Limit the number of simultaneous sessions per user.', 'vigilante' ); ?></p>
4818
4819 <table class="form-table">
4820 <tr>
4821 <th scope="row"><?php esc_html_e( 'Enable Session Limits', 'vigilante' ); ?></th>
4822 <td>
4823 <label>
4824 <input type="checkbox" name="user_security[session_limits][enabled]" value="1" <?php checked( ! empty( $session_limits['enabled'] ) ); ?>>
4825 <?php esc_html_e( 'Limit the number of active sessions per user', 'vigilante' ); ?>
4826 </label>
4827 </td>
4828 </tr>
4829 <tr>
4830 <th scope="row"><label for="vigilante-f-user-security-session-limits-max-sessions"><?php esc_html_e( 'Maximum Sessions', 'vigilante' ); ?></label></th>
4831 <td>
4832 <input id="vigilante-f-user-security-session-limits-max-sessions" 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">
4833 <?php esc_html_e( 'sessions per user', 'vigilante' ); ?>
4834 </td>
4835 </tr>
4836 <tr>
4837 <th scope="row"><label for="vigilante-f-user-security-session-limits-behavior"><?php esc_html_e( 'When Limit Exceeded', 'vigilante' ); ?></label></th>
4838 <td>
4839 <select id="vigilante-f-user-security-session-limits-behavior" name="user_security[session_limits][behavior]">
4840 <option value="block_new" <?php selected( ( $session_limits['behavior'] ?? 'close_oldest' ), 'block_new' ); ?>><?php esc_html_e( 'Block new login', 'vigilante' ); ?></option>
4841 <option value="close_oldest" <?php selected( ( $session_limits['behavior'] ?? 'close_oldest' ), 'close_oldest' ); ?>><?php esc_html_e( 'Close oldest session', 'vigilante' ); ?></option>
4842 </select>
4843 <p class="description"><?php esc_html_e( '"Close oldest" is recommended for security - ensures attackers cannot lock out legitimate users.', 'vigilante' ); ?></p>
4844 </td>
4845 </tr>
4846 <tr>
4847 <th scope="row"><?php esc_html_e( 'Exclude Administrators', 'vigilante' ); ?></th>
4848 <td>
4849 <label>
4850 <input type="checkbox" name="user_security[session_limits][exclude_admins]" value="1" <?php checked( ! empty( $session_limits['exclude_admins'] ) ); ?>>
4851 <?php esc_html_e( 'Do not apply session limits to administrators', 'vigilante' ); ?>
4852 </label>
4853 </td>
4854 </tr>
4855 </table>
4856 </div>
4857
4858 <!-- Password Expiration -->
4859 <div id="vigilante-section-users-password-exp" class="vigilante-settings-section">
4860 <h2>
4861 <?php esc_html_e( 'Password expiration', 'vigilante' ); ?>
4862 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4863 </h2>
4864 <p><?php esc_html_e( 'Force users to change their password periodically.', 'vigilante' ); ?></p>
4865
4866 <table class="form-table">
4867 <tr>
4868 <th scope="row"><?php esc_html_e( 'Enable Password Expiration', 'vigilante' ); ?></th>
4869 <td>
4870 <label>
4871 <input type="checkbox" name="user_security[password_expiration][enabled]" value="1" <?php checked( ! empty( $password_exp['enabled'] ) ); ?>>
4872 <?php esc_html_e( 'Force password change after a set number of days', 'vigilante' ); ?>
4873 </label>
4874 </td>
4875 </tr>
4876 <tr>
4877 <th scope="row"><label for="vigilante-f-user-security-password-expiration-expire-days"><?php esc_html_e( 'Expire After', 'vigilante' ); ?></label></th>
4878 <td>
4879 <input id="vigilante-f-user-security-password-expiration-expire-days" 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">
4880 <?php esc_html_e( 'days', 'vigilante' ); ?>
4881 <p class="description"><?php esc_html_e( 'PCI-DSS recommends 90 days.', 'vigilante' ); ?></p>
4882 </td>
4883 </tr>
4884 <tr>
4885 <th scope="row"><label for="vigilante-f-user-security-password-expiration-warning-days"><?php esc_html_e( 'Warning Period', 'vigilante' ); ?></label></th>
4886 <td>
4887 <input id="vigilante-f-user-security-password-expiration-warning-days" 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">
4888 <?php esc_html_e( 'days before expiration', 'vigilante' ); ?>
4889 <p class="description"><?php esc_html_e( 'Show warning notice this many days before password expires.', 'vigilante' ); ?></p>
4890 </td>
4891 </tr>
4892 <tr>
4893 <th scope="row"><label for="vigilante-f-user-security-password-expiration-password-history"><?php esc_html_e( 'Password History', 'vigilante' ); ?></label></th>
4894 <td>
4895 <input id="vigilante-f-user-security-password-expiration-password-history" 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">
4896 <?php esc_html_e( 'passwords to remember', 'vigilante' ); ?>
4897 <p class="description"><?php esc_html_e( 'Prevent reusing recent passwords. Set to 0 to disable.', 'vigilante' ); ?></p>
4898 </td>
4899 </tr>
4900 <tr>
4901 <th scope="row"><?php esc_html_e( 'Email Reminder', 'vigilante' ); ?></th>
4902 <td>
4903 <label>
4904 <input type="checkbox" name="user_security[password_expiration][send_reminder]" value="1" <?php checked( ! empty( $password_exp['send_reminder'] ) ); ?>>
4905 <?php esc_html_e( 'Send email reminder when password is about to expire', 'vigilante' ); ?>
4906 </label>
4907 <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>
4908 </td>
4909 </tr>
4910 <tr>
4911 <th scope="row"><?php esc_html_e( 'Affected Roles', 'vigilante' ); ?></th>
4912 <td>
4913 <?php
4914 $affected_roles = $password_exp['affected_roles'] ?? array( 'administrator', 'editor' );
4915 $all_roles = wp_roles()->get_names();
4916 foreach ( $all_roles as $role_slug => $role_name ) :
4917 ?>
4918 <label style="display: block; margin-bottom: 5px;">
4919 <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 ) ); ?>>
4920 <?php echo esc_html( translate_user_role( $role_name ) ); ?>
4921 </label>
4922 <?php endforeach; ?>
4923 </td>
4924 </tr>
4925 <tr>
4926 <th scope="row"><?php esc_html_e( 'Exclude specific users', 'vigilante' ); ?></th>
4927 <td>
4928 <?php $pwexp_excluded = $password_exp['excluded_users'] ?? array(); ?>
4929 <div class="vigilante-2fa-user-search-container">
4930 <div class="vigilante-2fa-user-search">
4931 <span class="search-icon"></span>
4932 <input type="text"
4933 id="vigilante_pwexp_user_search"
4934 placeholder="<?php esc_attr_e( 'Search users by name or email...', 'vigilante' ); ?>"
4935 autocomplete="off">
4936 <div class="vigilante-pwexp-search-results"></div>
4937 </div>
4938 <div class="vigilante-pwexp-excluded-users">
4939 <?php
4940 foreach ( $pwexp_excluded as $pwexp_excluded_id ) :
4941 $excluded_user = get_user_by( 'ID', $pwexp_excluded_id );
4942 if ( ! $excluded_user ) {
4943 continue;
4944 }
4945 ?>
4946 <div class="vigilante-pwexp-excluded-user" data-user-id="<?php echo esc_attr( $pwexp_excluded_id ); ?>">
4947 <span class="user-display"><?php echo esc_html( $excluded_user->display_name . ' (' . $excluded_user->user_email . ')' ); ?></span>
4948 <button type="button" class="remove-user" aria-label="<?php esc_attr_e( 'Remove', 'vigilante' ); ?>">&times;</button>
4949 <input type="hidden" name="user_security[password_expiration][excluded_users][]" value="<?php echo esc_attr( $pwexp_excluded_id ); ?>">
4950 </div>
4951 <?php endforeach; ?>
4952 </div>
4953 </div>
4954 <p class="description"><?php esc_html_e( 'Listed users will be excluded from password expiration regardless of their role.', 'vigilante' ); ?></p>
4955 </td>
4956 </tr>
4957 </table>
4958 </div>
4959
4960 <!-- Email Verification -->
4961 <div id="vigilante-section-users-email-verify" class="vigilante-settings-section">
4962 <h2>
4963 <?php esc_html_e( 'Email verification', 'vigilante' ); ?>
4964 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4965 </h2>
4966 <p><?php esc_html_e( 'Require new users to verify their email address before logging in.', 'vigilante' ); ?></p>
4967
4968 <table class="form-table">
4969 <tr>
4970 <th scope="row"><?php esc_html_e( 'Enable Email Verification', 'vigilante' ); ?></th>
4971 <td>
4972 <label>
4973 <input type="checkbox" name="user_security[email_verification][enabled]" value="1" <?php checked( ! empty( $email_verify['enabled'] ) ); ?>>
4974 <?php esc_html_e( 'New users must verify their email before logging in', 'vigilante' ); ?>
4975 </label>
4976 </td>
4977 </tr>
4978 <tr>
4979 <th scope="row"><label for="vigilante-f-user-security-email-verification-token-expiry-hours"><?php esc_html_e( 'Link Expiration', 'vigilante' ); ?></label></th>
4980 <td>
4981 <input id="vigilante-f-user-security-email-verification-token-expiry-hours" 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">
4982 <?php esc_html_e( 'hours', 'vigilante' ); ?>
4983 </td>
4984 </tr>
4985 <tr>
4986 <th scope="row"><?php esc_html_e( 'Allow Resend', 'vigilante' ); ?></th>
4987 <td>
4988 <label>
4989 <input type="checkbox" name="user_security[email_verification][allow_resend]" value="1" <?php checked( ! empty( $email_verify['allow_resend'] ) ); ?>>
4990 <?php esc_html_e( 'Allow users to request a new verification email', 'vigilante' ); ?>
4991 </label>
4992 </td>
4993 </tr>
4994 <tr>
4995 <th scope="row"><label for="vigilante-f-user-security-email-verification-auto-delete-days"><?php esc_html_e( 'Auto-delete Unverified', 'vigilante' ); ?></label></th>
4996 <td>
4997 <input id="vigilante-f-user-security-email-verification-auto-delete-days" 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">
4998 <?php esc_html_e( 'days (0 = never)', 'vigilante' ); ?>
4999 <p class="description"><?php esc_html_e( 'Automatically delete users who never verify their email.', 'vigilante' ); ?></p>
5000 </td>
5001 </tr>
5002 </table>
5003 </div>
5004
5005 <p class="submit vigilante-submit-buttons">
5006 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
5007 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
5008 </button>
5009 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
5010 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
5011 </button>
5012 </p>
5013 </form>
5014
5015 <!-- ============================================================
5016 TOOLS SECTION - Actions and utilities (no save button)
5017 ============================================================ -->
5018 <div class="vigilante-tools-section">
5019 <h2 class="vigilante-tools-header">
5020 <?php esc_html_e( 'User security tools', 'vigilante' ); ?>
5021 </h2>
5022
5023 <?php $this->render_user_actions_notice(); ?>
5024 <?php if ( ! $this->user_actions_locked() ) : ?>
5025
5026 <!-- Force Password Reset -->
5027 <div class="vigilante-tool-box">
5028 <h3><?php esc_html_e( 'Force password reset', 'vigilante' ); ?></h3>
5029 <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>
5030
5031 <!-- Reset Specific Users -->
5032 <div class="vigilante-password-reset-box">
5033 <h4><?php esc_html_e( 'Reset specific users', 'vigilante' ); ?></h4>
5034
5035 <div class="vigilante-user-search-wrapper">
5036 <input type="text" id="vigilante-password-reset-search" class="regular-text" placeholder="<?php esc_attr_e( 'Search by username, email, or display name...', 'vigilante' ); ?>">
5037 <div id="vigilante-password-reset-results" class="vigilante-user-search-results" style="display: none;"></div>
5038 </div>
5039
5040 <div id="vigilante-password-reset-selected" class="vigilante-selected-users" style="display: none;">
5041 <strong><?php esc_html_e( 'Selected Users:', 'vigilante' ); ?></strong>
5042 <ul class="vigilante-selected-users-list"></ul>
5043 </div>
5044
5045 <p class="description" style="margin-top: 15px;">
5046 <span class="dashicons dashicons-email-alt" style="color: #2271b1;"></span>
5047 <?php esc_html_e( 'Selected users will receive an email with a password reset link.', 'vigilante' ); ?>
5048 </p>
5049
5050 <p class="submit">
5051 <button type="button" id="vigilante-reset-selected-users" class="button button-primary" disabled>
5052 <?php esc_html_e( 'Force Reset for Selected Users', 'vigilante' ); ?>
5053 </button>
5054 </p>
5055 </div>
5056
5057 <!-- Reset by Role -->
5058 <div class="vigilante-password-reset-box" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
5059 <h4><?php esc_html_e( 'Reset by role', 'vigilante' ); ?></h4>
5060 <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>
5061
5062 <?php
5063 $wp_roles = wp_roles();
5064 $user_counts = count_users();
5065 $avail_roles = $user_counts['avail_roles'] ?? array();
5066 $current_user = wp_get_current_user();
5067 $current_roles = $current_user->roles;
5068 ?>
5069
5070 <fieldset class="vigilante-role-checkboxes" style="margin-top: 10px;">
5071 <?php foreach ( $wp_roles->roles as $role_slug => $role_data ) :
5072 $count = $avail_roles[ $role_slug ] ?? 0;
5073 if ( 0 === $count ) {
5074 continue;
5075 }
5076 $role_name = translate_user_role( $role_data['name'] );
5077 ?>
5078 <label style="display: block; margin-bottom: 6px;">
5079 <input type="checkbox"
5080 class="vigilante-reset-role-checkbox"
5081 value="<?php echo esc_attr( $role_slug ); ?>"
5082 data-count="<?php echo absint( $count ); ?>">
5083 <?php
5084 printf(
5085 /* translators: 1: Role name, 2: Number of users */
5086 '%1$s <span class="description">(%2$d)</span>',
5087 esc_html( $role_name ),
5088 absint( $count )
5089 );
5090 ?>
5091 <?php if ( in_array( $role_slug, $current_roles, true ) ) : ?>
5092 <em class="description"><?php esc_html_e( '(includes you)', 'vigilante' ); ?></em>
5093 <?php endif; ?>
5094 </label>
5095 <?php endforeach; ?>
5096 </fieldset>
5097
5098 <div id="vigilante-reset-role-summary" style="display: none; margin-top: 10px;">
5099 <p>
5100 <span class="dashicons dashicons-groups" style="color: #2271b1;"></span>
5101 <strong id="vigilante-reset-role-count">0</strong>
5102 <?php esc_html_e( 'user(s) will be affected.', 'vigilante' ); ?>
5103 </p>
5104 </div>
5105
5106 <div class="vigilante-password-reset-options" id="vigilante-reset-role-self-option" style="display: none; margin-top: 10px;">
5107 <label>
5108 <input type="checkbox" id="vigilante-reset-role-include-self" value="1">
5109 <?php esc_html_e( 'Include myself (your current session will end)', 'vigilante' ); ?>
5110 </label>
5111 </div>
5112
5113 <p class="submit">
5114 <button type="button" id="vigilante-reset-by-role" class="button button-primary" disabled>
5115 <?php esc_html_e( 'Force Reset for Selected Roles', 'vigilante' ); ?>
5116 </button>
5117 </p>
5118 </div>
5119
5120 <!-- Reset All Users -->
5121 <div class="vigilante-password-reset-box" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
5122 <h4><?php esc_html_e( 'Reset all users', 'vigilante' ); ?></h4>
5123
5124 <?php
5125 $total_users = count_users();
5126 $total_count = $total_users['total_users'];
5127 ?>
5128 <p>
5129 <?php
5130 printf(
5131 /* translators: %d: Number of users */
5132 esc_html__( 'This will affect %d user(s).', 'vigilante' ),
5133 absint( $total_count )
5134 );
5135 ?>
5136 </p>
5137
5138 <p class="description" style="color: #d63638;">
5139 <span class="dashicons dashicons-warning"></span>
5140 <?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' ); ?>
5141 </p>
5142
5143 <div class="vigilante-password-reset-options" style="margin-top: 10px;">
5144 <label>
5145 <input type="checkbox" id="vigilante-reset-all-include-self" value="1">
5146 <?php esc_html_e( 'Include myself (your current session will end)', 'vigilante' ); ?>
5147 </label>
5148 </div>
5149
5150 <p class="submit">
5151 <button type="button" id="vigilante-reset-all-users" class="button" style="color: #d63638; border-color: #d63638;">
5152 <?php esc_html_e( 'Force Reset for ALL Users', 'vigilante' ); ?>
5153 </button>
5154 </p>
5155 </div>
5156 </div>
5157
5158 <!-- Pending Registrations -->
5159 <?php
5160 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
5161 $pending_users = $user_security->get_pending_users();
5162 ?>
5163 <div id="vigilante-section-users-pending" class="vigilante-tool-box vigilante-pending-users-section">
5164 <h3>
5165 <?php esc_html_e( 'Pending registrations', 'vigilante' ); ?>
5166 <?php if ( count( $pending_users ) > 0 ) : ?>
5167 <span class="vigilante-badge vigilante-badge-warning"><?php echo esc_html( count( $pending_users ) ); ?></span>
5168 <?php endif; ?>
5169 </h3>
5170
5171 <?php if ( empty( $registration['enabled'] ) ) : ?>
5172 <p class="description">
5173 <span class="dashicons dashicons-info" style="color: #72aee6;"></span>
5174 <?php esc_html_e( 'Registration approval is disabled. Enable it in the settings above to require manual approval for new users.', 'vigilante' ); ?>
5175 </p>
5176 <?php elseif ( empty( $pending_users ) ) : ?>
5177 <div class="vigilante-no-lockouts">
5178 <span class="dashicons dashicons-yes-alt"></span>
5179 <p><?php esc_html_e( 'No pending registrations.', 'vigilante' ); ?></p>
5180 </div>
5181 <?php else : ?>
5182 <?php $this->render_user_actions_notice(); ?>
5183 <table class="wp-list-table widefat fixed striped vigilante-pending-users-table">
5184 <thead>
5185 <tr>
5186 <th><?php esc_html_e( 'User', 'vigilante' ); ?></th>
5187 <th><?php esc_html_e( 'Email', 'vigilante' ); ?></th>
5188 <th><?php esc_html_e( 'Registered', 'vigilante' ); ?></th>
5189 <th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
5190 </tr>
5191 </thead>
5192 <tbody>
5193 <?php foreach ( $pending_users as $pending_user ) :
5194 $pending_since = get_user_meta( $pending_user->ID, 'vigilante_pending_since', true );
5195 ?>
5196 <tr data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>">
5197 <td>
5198 <?php echo get_avatar( $pending_user->ID, 32 ); ?>
5199 <strong><?php echo esc_html( $pending_user->user_login ); ?></strong>
5200 </td>
5201 <td><?php echo esc_html( $pending_user->user_email ); ?></td>
5202 <td>
5203 <?php
5204 if ( $pending_since ) {
5205 /* translators: %s: Time ago */
5206 printf( esc_html__( '%s ago', 'vigilante' ), esc_html( human_time_diff( $pending_since ) ) );
5207 } else {
5208 echo esc_html( $pending_user->user_registered );
5209 }
5210 ?>
5211 </td>
5212 <td>
5213 <button type="button" class="button button-small vigilante-approve-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>" <?php disabled( $this->user_actions_locked() ); ?>>
5214 <?php esc_html_e( 'Approve', 'vigilante' ); ?>
5215 </button>
5216 <button type="button" class="button button-small vigilante-reject-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>" style="color: #d63638;" <?php disabled( $this->user_actions_locked() ); ?>>
5217 <?php esc_html_e( 'Reject', 'vigilante' ); ?>
5218 </button>
5219 </td>
5220 </tr>
5221 <?php endforeach; ?>
5222 </tbody>
5223 </table>
5224 <?php endif; ?>
5225 </div>
5226
5227 <!-- Active Sessions Management -->
5228 <div class="vigilante-tool-box vigilante-session-management-section">
5229 <h3><?php esc_html_e( 'Active sessions', 'vigilante' ); ?></h3>
5230 <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>
5231
5232 <!-- Current user sessions -->
5233 <h4><?php esc_html_e( 'Your sessions', 'vigilante' ); ?></h4>
5234 <?php
5235 $current_user_id = get_current_user_id();
5236 $my_sessions = $user_security->get_user_sessions( $current_user_id );
5237 $has_corrupted = $user_security->has_corrupted_sessions( $current_user_id );
5238 $raw_count = $user_security->get_raw_session_count( $current_user_id );
5239 ?>
5240
5241 <?php if ( $has_corrupted && $raw_count > 0 ) : ?>
5242 <div class="notice notice-warning inline" style="margin: 10px 0;">
5243 <p>
5244 <span class="dashicons dashicons-warning" style="color: #dba617;"></span>
5245 <?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' ); ?>
5246 </p>
5247 </div>
5248 <?php endif; ?>
5249
5250 <?php if ( empty( $my_sessions ) ) : ?>
5251 <p class="description"><?php esc_html_e( 'No active sessions found.', 'vigilante' ); ?></p>
5252 <?php if ( $has_corrupted ) : ?>
5253 <p style="margin-top: 10px;">
5254 <button type="button" class="button vigilante-revoke-other-sessions" data-user-id="<?php echo esc_attr( $current_user_id ); ?>">
5255 <?php esc_html_e( 'Clean Up Corrupted Sessions', 'vigilante' ); ?>
5256 </button>
5257 </p>
5258 <?php endif; ?>
5259 <?php else : ?>
5260 <div class="vigilante-paginated-section">
5261 <div class="vigilante-fi-pagination-wrap"></div>
5262 <table class="wp-list-table widefat fixed striped vigilante-sessions-table vigilante-fi-paginated">
5263 <thead>
5264 <tr>
5265 <th><?php esc_html_e( 'Browser', 'vigilante' ); ?></th>
5266 <th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th>
5267 <th><?php esc_html_e( 'Login Time', 'vigilante' ); ?></th>
5268 <th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
5269 </tr>
5270 </thead>
5271 <tbody>
5272 <?php foreach ( $my_sessions as $session ) : ?>
5273 <tr data-token="<?php echo esc_attr( $session['token_hash'] ); ?>">
5274 <td><?php echo esc_html( $session['browser'] ); ?></td>
5275 <td><code><?php echo esc_html( $session['ip'] ); ?></code></td>
5276 <td>
5277 <?php
5278 if ( $session['login'] ) {
5279 /* translators: %s: Time ago */
5280 printf( esc_html__( '%s ago', 'vigilante' ), esc_html( human_time_diff( $session['login'] ) ) );
5281 } else {
5282 esc_html_e( 'Unknown', 'vigilante' );
5283 }
5284 ?>
5285 </td>
5286 <td>
5287 <?php if ( ! $session['is_current'] ) : ?>
5288 <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'] ); ?>">
5289 <?php esc_html_e( 'Revoke', 'vigilante' ); ?>
5290 </button>
5291 <?php else : ?>
5292 <span class="description"><?php esc_html_e( 'Current session', 'vigilante' ); ?></span>
5293 <?php endif; ?>
5294 </td>
5295 </tr>
5296 <?php endforeach; ?>
5297 </tbody>
5298 </table>
5299 </div>
5300
5301 <?php if ( count( $my_sessions ) > 1 || $has_corrupted ) : ?>
5302 <p style="margin-top: 10px;">
5303 <button type="button" class="button vigilante-revoke-other-sessions" data-user-id="<?php echo esc_attr( $current_user_id ); ?>">
5304 <?php esc_html_e( 'Revoke All Other Sessions', 'vigilante' ); ?>
5305 </button>
5306 </p>
5307 <?php endif; ?>
5308 <?php endif; ?>
5309
5310 <!-- Search user sessions (admin only) -->
5311 <h4 style="margin-top: 30px;"><?php esc_html_e( 'Manage user sessions', 'vigilante' ); ?></h4>
5312 <p class="description"><?php esc_html_e( 'Search for a user to view and manage their sessions.', 'vigilante' ); ?></p>
5313
5314 <div class="vigilante-user-search-wrapper" style="margin-top: 10px;">
5315 <input type="text" id="vigilante-session-user-search" class="regular-text" placeholder="<?php esc_attr_e( 'Search by username or email...', 'vigilante' ); ?>">
5316 <div id="vigilante-session-search-results" class="vigilante-user-search-results" style="display: none;"></div>
5317 </div>
5318
5319 <div id="vigilante-user-sessions-container" style="display: none; margin-top: 20px;">
5320 <h4 id="vigilante-sessions-user-name"></h4>
5321 <table class="wp-list-table widefat fixed striped vigilante-sessions-table">
5322 <thead>
5323 <tr>
5324 <th><?php esc_html_e( 'Browser', 'vigilante' ); ?></th>
5325 <th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th>
5326 <th><?php esc_html_e( 'Login Time', 'vigilante' ); ?></th>
5327 <th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
5328 </tr>
5329 </thead>
5330 <tbody id="vigilante-user-sessions-list">
5331 </tbody>
5332 </table>
5333 <p style="margin-top: 10px;">
5334 <button type="button" class="button vigilante-revoke-all-user-sessions" style="color: #d63638;">
5335 <?php esc_html_e( 'Revoke All Sessions', 'vigilante' ); ?>
5336 </button>
5337 </p>
5338 </div>
5339 </div>
5340
5341 <?php endif; ?>
5342 </div>
5343 <?php
5344 }
5345
5346 /**
5347 * Render WordPress Hardening tab
5348 */
5349 private function render_tab_wp_hardening() {
5350 $is_disabled = $this->render_module_disabled_notice( 'wp_hardening' );
5351 $options = $this->settings->get_section( 'wp_hardening' );
5352 ?>
5353 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="wp_hardening" <?php echo $is_disabled ? 'inert' : ''; ?>>
5354 <!-- Database Hardening (outside form save flow - uses its own AJAX action) -->
5355 <?php $vg_shared_locked = $this->shared_files_locked(); ?>
5356 <?php $this->render_shared_files_notice(); ?>
5357 <div id="vigilante-section-hardening-database" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
5358 <h2>
5359 <?php esc_html_e( 'Database Hardening', 'vigilante' ); ?>
5360 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
5361 <span class="vigilante-method-badge config"><?php esc_html_e( 'WP-CONFIG', 'vigilante' ); ?></span>
5362 </h2>
5363 <p><?php esc_html_e( 'Change the database table prefix to prevent SQL injection attacks that target default WordPress tables.', 'vigilante' ); ?></p>
5364
5365 <?php
5366 $db_prefix = new Vigilante_Database_Prefix();
5367 $current_prefix = $db_prefix->get_current_prefix();
5368 $is_default = $db_prefix->is_default_prefix();
5369 ?>
5370
5371 <?php if ( is_multisite() && ! $vg_shared_locked ) : ?>
5372 <div class="notice notice-warning inline" style="margin:10px 0 16px;padding:8px 12px;">
5373 <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>
5374 </div>
5375 <?php endif; ?>
5376
5377 <table class="form-table">
5378 <tr>
5379 <th scope="row"><?php esc_html_e( 'Current prefix', 'vigilante' ); ?></th>
5380 <td>
5381 <code class="vigilante-db-current-prefix"><?php echo esc_html( $current_prefix ); ?></code>
5382 <?php if ( $is_default ) : ?>
5383 <span class="vigilante-inline-warning">
5384 <span class="dashicons dashicons-warning"></span>
5385 <?php esc_html_e( 'Default prefix detected. Changing it adds a layer of protection against automated SQL injection attacks.', 'vigilante' ); ?>
5386 </span>
5387 <?php else : ?>
5388 <span class="vigilante-inline-ok">
5389 <span class="dashicons dashicons-yes-alt"></span>
5390 <?php esc_html_e( 'Custom prefix in use.', 'vigilante' ); ?>
5391 </span>
5392 <?php endif; ?>
5393 </td>
5394 </tr>
5395 <tr>
5396 <th scope="row"><?php esc_html_e( 'New prefix', 'vigilante' ); ?></th>
5397 <td>
5398 <div class="vigilante-db-prefix-row">
5399 <code class="vigilante-db-new-prefix" id="vigilante-new-prefix"><?php echo esc_html( $db_prefix->generate_prefix() ); ?></code>
5400 <button type="button" class="button button-small vigilante-db-regenerate-prefix" title="<?php esc_attr_e( 'Generate new prefix', 'vigilante' ); ?>">
5401 <span class="dashicons dashicons-update"></span>
5402 </button>
5403 </div>
5404 </td>
5405 </tr>
5406 <tr>
5407 <th scope="row"></th>
5408 <td>
5409 <div class="vigilante-db-prefix-confirm">
5410 <label>
5411 <input type="checkbox" id="vigilante-prefix-backup-confirm">
5412 <?php esc_html_e( 'I understand this operation is irreversible and I have a current database backup', 'vigilante' ); ?>
5413 </label>
5414 <p class="description">
5415 <?php
5416 printf(
5417 /* translators: %s: Link to tools tab */
5418 esc_html__( 'Need a backup? %s first.', 'vigilante' ),
5419 '<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Download a database backup', 'vigilante' ) . '</a>'
5420 );
5421 ?>
5422 </p>
5423 </div>
5424 <button type="button" class="button button-primary vigilante-db-change-prefix" disabled data-original-text="<?php esc_attr_e( 'Change Database Prefix', 'vigilante' ); ?>">
5425 <?php esc_html_e( 'Change Database Prefix', 'vigilante' ); ?>
5426 </button>
5427 </td>
5428 </tr>
5429 </table>
5430 </div>
5431
5432 <!-- wp-config Security -->
5433 <?php
5434 $vg_shared_locked = $this->shared_files_locked();
5435 // Paint what is actually in force, not this site's unused copy.
5436 $vg_local_options = $options;
5437 $options = $this->get_section_for_display( 'wp_hardening' );
5438 ?>
5439 <?php $this->render_shared_files_notice(); ?>
5440 <div id="vigilante-section-hardening-wpconfig" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
5441 <h2>
5442 <?php esc_html_e( 'wp-config.php Security', 'vigilante' ); ?>
5443 <span class="vigilante-method-badge config"><?php esc_html_e( 'WP-CONFIG', 'vigilante' ); ?></span>
5444 </h2>
5445 <p><?php esc_html_e( 'Security constants added directly to wp-config.php file.', 'vigilante' ); ?></p>
5446
5447 <table class="form-table">
5448 <tr>
5449 <th scope="row"><?php esc_html_e( 'Disable File Editor', 'vigilante' ); ?></th>
5450 <td>
5451 <label>
5452 <input type="checkbox" name="wp_hardening[disallow_file_edit]" value="1" <?php checked( ! empty( $options['disallow_file_edit'] ) ); ?>>
5453 <?php esc_html_e( 'Disable plugin and theme editor in admin (DISALLOW_FILE_EDIT)', 'vigilante' ); ?>
5454 </label>
5455 </td>
5456 </tr>
5457 <tr>
5458 <th scope="row"><?php esc_html_e( 'Disable File Modifications', 'vigilante' ); ?></th>
5459 <td>
5460 <label>
5461 <input type="checkbox" name="wp_hardening[disallow_file_mods]" value="1" <?php checked( ! empty( $options['disallow_file_mods'] ) ); ?>>
5462 <?php esc_html_e( 'Disable all file modifications including updates (DISALLOW_FILE_MODS)', 'vigilante' ); ?>
5463 </label>
5464 <p class="description"><?php esc_html_e( '&#9888; Warning: This prevents automatic updates.', 'vigilante' ); ?></p>
5465 </td>
5466 </tr>
5467 <tr id="field-force-ssl-admin">
5468 <th scope="row"><?php esc_html_e( 'Force SSL Admin', 'vigilante' ); ?></th>
5469 <td>
5470 <label>
5471 <input type="checkbox" name="wp_hardening[force_ssl_admin]" value="1" <?php checked( ! empty( $options['force_ssl_admin'] ) ); ?>>
5472 <?php esc_html_e( 'Force HTTPS for admin area (FORCE_SSL_ADMIN)', 'vigilante' ); ?>
5473 </label>
5474 <p class="description"><?php esc_html_e( '&#9888; Warning: Only enable if your site fully supports HTTPS.', 'vigilante' ); ?></p>
5475 </td>
5476 </tr>
5477 <tr id="field-wp-debug">
5478 <th scope="row"><?php esc_html_e( 'Hide PHP errors from visitors', 'vigilante' ); ?></th>
5479 <td>
5480 <label>
5481 <input type="checkbox" name="wp_hardening[wp_debug]" value="1" <?php checked( ! empty( $options['wp_debug'] ) ); ?>>
5482 <?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' ); ?>
5483 </label>
5484 </td>
5485 </tr>
5486 <tr id="field-disable-wp-cron">
5487 <th scope="row"><?php esc_html_e( 'Disable WP Cron', 'vigilante' ); ?></th>
5488 <td>
5489 <label>
5490 <input type="checkbox" name="wp_hardening[disable_wp_cron]" value="1" <?php checked( ! empty( $options['disable_wp_cron'] ) ); ?>>
5491 <?php esc_html_e( 'Disable WordPress\'s page-view cron trigger (DISABLE_WP_CRON)', 'vigilante' ); ?>
5492 </label>
5493 <p class="description"><?php
5494 printf(
5495 /* translators: 1: opening <strong>, 2: closing </strong>, 3: opening <code>, 4: closing </code> */
5496 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' ),
5497 '<strong>',
5498 '</strong>',
5499 '<code>',
5500 '</code>'
5501 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded.
5502 ?></p>
5503 </td>
5504 </tr>
5505 </table>
5506 </div>
5507 <?php $options = $vg_local_options; ?>
5508
5509 <!-- Comment Security -->
5510 <div id="vigilante-section-hardening-xmlrpc" class="vigilante-settings-section">
5511 <h2>
5512 <?php esc_html_e( 'XML-RPC', 'vigilante' ); ?>
5513 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5514 </h2>
5515 <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>
5516
5517 <table class="form-table">
5518 <tr id="field-disable-xmlrpc">
5519 <th scope="row"><label for="vigilante-f-wp-hardening-xmlrpc-mode"><?php esc_html_e( 'XML-RPC access', 'vigilante' ); ?></label></th>
5520 <td>
5521 <?php $vig_xmlrpc_mode = Vigilante_Comment_Security::resolve_xmlrpc_mode( $this->settings ); ?>
5522 <select id="vigilante-f-wp-hardening-xmlrpc-mode" name="wp_hardening[xmlrpc_mode]">
5523 <option value="none" <?php selected( $vig_xmlrpc_mode, 'none' ); ?>>
5524 <?php esc_html_e( 'Leave XML-RPC enabled', 'vigilante' ); ?>
5525 </option>
5526 <option value="pingback" <?php selected( $vig_xmlrpc_mode, 'pingback' ); ?>>
5527 <?php esc_html_e( 'Block the pingback methods only', 'vigilante' ); ?>
5528 </option>
5529 <option value="full" <?php selected( $vig_xmlrpc_mode, 'full' ); ?>>
5530 <?php esc_html_e( 'Disable XML-RPC completely (recommended)', 'vigilante' ); ?>
5531 </option>
5532 </select>
5533 <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>
5534 </td>
5535 </tr>
5536 </table>
5537 </div>
5538
5539 <div id="vigilante-section-hardening-comments" class="vigilante-settings-section">
5540 <h2>
5541 <?php esc_html_e( 'Comment Security', 'vigilante' ); ?>
5542 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5543 <span class="vigilante-method-badge settings"><?php esc_html_e( 'Settings', 'vigilante' ); ?></span>
5544 </h2>
5545 <p><?php esc_html_e( 'Comment protection using WordPress settings and PHP hooks.', 'vigilante' ); ?></p>
5546
5547 <table class="form-table">
5548 <tr>
5549 <th scope="row"><?php esc_html_e( 'Disable Pingbacks', 'vigilante' ); ?></th>
5550 <td>
5551 <label>
5552 <input type="checkbox" name="wp_hardening[disable_pingbacks]" value="1" <?php checked( ! empty( $options['disable_pingbacks'] ) ); ?>>
5553 <?php esc_html_e( 'Disable pingbacks (commonly exploited for DDoS)', 'vigilante' ); ?>
5554 </label>
5555 </td>
5556 </tr>
5557 <tr>
5558 <th scope="row"><?php esc_html_e( 'Disable Trackbacks', 'vigilante' ); ?></th>
5559 <td>
5560 <label>
5561 <input type="checkbox" name="wp_hardening[disable_trackbacks]" value="1" <?php checked( ! empty( $options['disable_trackbacks'] ) ); ?>>
5562 <?php esc_html_e( 'Disable trackbacks (rarely used legitimately)', 'vigilante' ); ?>
5563 </label>
5564 </td>
5565 </tr>
5566 <tr>
5567 <th scope="row"><?php esc_html_e( 'Require Moderation', 'vigilante' ); ?></th>
5568 <td>
5569 <label>
5570 <input type="checkbox" name="wp_hardening[require_comment_moderation]" value="1" <?php checked( ! empty( $options['require_comment_moderation'] ) ); ?>>
5571 <?php esc_html_e( 'All comments must be manually approved', 'vigilante' ); ?>
5572 </label>
5573 </td>
5574 </tr>
5575 <tr>
5576 <th scope="row"><?php esc_html_e( 'Close Old Comments', 'vigilante' ); ?></th>
5577 <td>
5578 <label>
5579 <input type="checkbox" name="wp_hardening[close_old_comments]" value="1" <?php checked( ! empty( $options['close_old_comments'] ) ); ?>>
5580 <?php esc_html_e( 'Automatically close comments on old posts after', 'vigilante' ); ?>
5581 </label>
5582 <input type="number" id="vigilante-f-wp-hardening-close-comments-after-days" 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">
5583 <label for="vigilante-f-wp-hardening-close-comments-after-days"><?php esc_html_e( 'days', 'vigilante' ); ?></label>
5584 </td>
5585 </tr>
5586 <tr>
5587 <th scope="row"><?php esc_html_e( 'Honeypot Protection', 'vigilante' ); ?></th>
5588 <td>
5589 <label>
5590 <input type="checkbox" name="wp_hardening[honeypot_comments]" value="1" <?php checked( ! empty( $options['honeypot_comments'] ) ); ?>>
5591 <?php esc_html_e( 'Add hidden honeypot field to catch bots', 'vigilante' ); ?>
5592 </label>
5593 </td>
5594 </tr>
5595 </table>
5596 </div>
5597
5598 <!-- Head Cleaner -->
5599 <div id="vigilante-section-hardening-headers" class="vigilante-settings-section">
5600 <h2>
5601 <?php esc_html_e( 'Header Cleanup', 'vigilante' ); ?>
5602 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5603 </h2>
5604 <p><?php esc_html_e( 'Remove meta tags from HTML head.', 'vigilante' ); ?></p>
5605
5606 <table class="form-table">
5607 <tr>
5608 <th scope="row"><?php esc_html_e( 'Remove Generator', 'vigilante' ); ?></th>
5609 <td>
5610 <label>
5611 <input type="checkbox" name="wp_hardening[remove_wp_generator]" value="1" <?php checked( ! empty( $options['remove_wp_generator'] ) ); ?>>
5612 <?php esc_html_e( 'Remove WordPress version from HTML head', 'vigilante' ); ?>
5613 </label>
5614 </td>
5615 </tr>
5616 <tr id="field-remove-wp-version-assets">
5617 <th scope="row"><?php esc_html_e( 'Remove version from assets', 'vigilante' ); ?></th>
5618 <td>
5619 <label>
5620 <input type="checkbox" name="wp_hardening[remove_wp_version_assets]" value="1" <?php checked( ! empty( $options['remove_wp_version_assets'] ) ); ?>>
5621 <?php esc_html_e( 'Remove WordPress version from script/style URLs (?ver=)', 'vigilante' ); ?>
5622 </label>
5623 <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>
5624 </td>
5625 </tr>
5626 <tr>
5627 <th scope="row"><?php esc_html_e( 'Remove RSD Link', 'vigilante' ); ?></th>
5628 <td>
5629 <label>
5630 <input type="checkbox" name="wp_hardening[remove_rsd_link]" value="1" <?php checked( ! empty( $options['remove_rsd_link'] ) ); ?>>
5631 <?php esc_html_e( 'Remove Really Simple Discovery link', 'vigilante' ); ?>
5632 </label>
5633 </td>
5634 </tr>
5635 <tr>
5636 <th scope="row"><?php esc_html_e( 'Remove WLW Manifest', 'vigilante' ); ?></th>
5637 <td>
5638 <label>
5639 <input type="checkbox" name="wp_hardening[remove_wlw_manifest]" value="1" <?php checked( ! empty( $options['remove_wlw_manifest'] ) ); ?>>
5640 <?php esc_html_e( 'Remove Windows Live Writer manifest link', 'vigilante' ); ?>
5641 </label>
5642 </td>
5643 </tr>
5644 <tr>
5645 <th scope="row"><?php esc_html_e( 'Remove Shortlink', 'vigilante' ); ?></th>
5646 <td>
5647 <label>
5648 <input type="checkbox" name="wp_hardening[remove_shortlink]" value="1" <?php checked( ! empty( $options['remove_shortlink'] ) ); ?>>
5649 <?php esc_html_e( 'Remove shortlink tag from header', 'vigilante' ); ?>
5650 </label>
5651 </td>
5652 </tr>
5653 <tr>
5654 <th scope="row"><?php esc_html_e( 'Remove REST API Link', 'vigilante' ); ?></th>
5655 <td>
5656 <label>
5657 <input type="checkbox" name="wp_hardening[remove_rest_api_link]" value="1" <?php checked( ! empty( $options['remove_rest_api_link'] ) ); ?>>
5658 <?php esc_html_e( 'Remove REST API discovery link from header', 'vigilante' ); ?>
5659 </label>
5660 <p class="description"><?php esc_html_e( '&#9888; Notice: Some plugins may need this link.', 'vigilante' ); ?></p>
5661 </td>
5662 </tr>
5663 </table>
5664 </div>
5665
5666 <!-- Feed Manager -->
5667 <div id="vigilante-section-hardening-rss" class="vigilante-settings-section">
5668 <h2>
5669 <?php esc_html_e( 'RSS Feed Settings', 'vigilante' ); ?>
5670 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5671 </h2>
5672 <p><?php esc_html_e( 'Control RSS/Atom feeds.', 'vigilante' ); ?></p>
5673
5674 <table class="form-table">
5675 <tr>
5676 <th scope="row"><?php esc_html_e( 'Disable Feeds', 'vigilante' ); ?></th>
5677 <td>
5678 <label>
5679 <input type="checkbox" name="wp_hardening[disable_feeds]" value="1" <?php checked( ! empty( $options['disable_feeds'] ) ); ?>>
5680 <?php esc_html_e( 'Completely disable RSS/Atom feeds', 'vigilante' ); ?>
5681 </label>
5682 </td>
5683 </tr>
5684 <tr>
5685 <th scope="row"><?php esc_html_e( 'Disable If No Content', 'vigilante' ); ?></th>
5686 <td>
5687 <label>
5688 <input type="checkbox" name="wp_hardening[disable_if_no_content]" value="1" <?php checked( ! empty( $options['disable_if_no_content'] ) ); ?>>
5689 <?php esc_html_e( 'Only disable feeds if site has no published posts', 'vigilante' ); ?>
5690 </label>
5691 </td>
5692 </tr>
5693 <tr>
5694 <th scope="row"><?php esc_html_e( 'Remove Feed Version', 'vigilante' ); ?></th>
5695 <td>
5696 <label>
5697 <input type="checkbox" name="wp_hardening[remove_feed_version]" value="1" <?php checked( ! empty( $options['remove_feed_version'] ) ); ?>>
5698 <?php esc_html_e( 'Remove WordPress version from feed generator tag', 'vigilante' ); ?>
5699 </label>
5700 </td>
5701 </tr>
5702 </table>
5703 </div>
5704
5705 <p class="submit vigilante-submit-buttons">
5706 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
5707 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
5708 </button>
5709 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
5710 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
5711 </button>
5712 </p>
5713 </form>
5714 <?php
5715 }
5716
5717 /**
5718 * Render activity log tab
5719 */
5720 private function render_tab_activity_log() {
5721 $is_disabled = $this->render_module_disabled_notice( 'activity_log' );
5722 $options = $this->settings->get_section( 'activity_log' );
5723 ?>
5724 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="activity_log" <?php echo $is_disabled ? 'inert' : ''; ?>>
5725 <div id="vigilante-section-audit-settings" class="vigilante-settings-section">
5726 <h2>
5727 <?php esc_html_e( 'Security Audit Settings', 'vigilante' ); ?>
5728 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5729 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
5730 </h2>
5731 <p><?php esc_html_e( 'Security event logging and auditing.', 'vigilante' ); ?></p>
5732
5733 <table class="form-table">
5734 <tr>
5735 <th scope="row"><?php esc_html_e( 'Retention', 'vigilante' ); ?></th>
5736 <td>
5737 <input type="number" id="vigilante-f-activity-log-retention-days" name="activity_log[retention_days]" value="<?php echo esc_attr( $options['retention_days'] ?? 30 ); ?>" min="7" max="365" class="small-text">
5738 <label for="vigilante-f-activity-log-retention-days"><?php esc_html_e( 'days', 'vigilante' ); ?></label>
5739 &nbsp;&nbsp;
5740 <input type="number" id="vigilante-f-activity-log-max-entries" name="activity_log[max_entries]" value="<?php echo esc_attr( $options['max_entries'] ?? 10000 ); ?>" min="100" max="100000" step="100" class="small-text">
5741 <label for="vigilante-f-activity-log-max-entries"><?php esc_html_e( 'max entries', 'vigilante' ); ?></label>
5742 <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>
5743 </td>
5744 </tr>
5745 <tr>
5746 <th scope="row"><?php esc_html_e( 'Events to Log', 'vigilante' ); ?></th>
5747 <td>
5748 <fieldset style="display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)); gap:6px 24px; max-width:600px;">
5749 <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>
5750 <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>
5751 <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>
5752 <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>
5753 <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>
5754 <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>
5755 <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>
5756 <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>
5757 <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>
5758 <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>
5759 </fieldset>
5760 <div class="notice notice-info inline" style="margin:10px 0 0;padding:8px 12px;">
5761 <p style="margin:0;">
5762 <?php esc_html_e( 'Firewall blocks, security events, and Vigilant settings changes are always logged regardless of the above selections.', 'vigilante' ); ?>
5763 </p>
5764 </div>
5765 </td>
5766 </tr>
5767 <tr>
5768 <th scope="row"><label for="vigilante-f-activity-log-tracked-options"><?php esc_html_e( 'Option Tracking', 'vigilante' ); ?></label></th>
5769 <td>
5770 <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>
5771 <br>
5772 <label><?php esc_html_e( 'Additional options to track:', 'vigilante' ); ?></label><br>
5773 <textarea id="vigilante-f-activity-log-tracked-options" 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>
5774 <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>
5775 </td>
5776 </tr>
5777 <tr>
5778 <th scope="row"><?php esc_html_e( 'Exclusions', 'vigilante' ); ?></th>
5779 <td>
5780 <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:16px; max-width:600px;">
5781 <div>
5782 <label for="vigilante-f-activity-log-excluded-users"><?php esc_html_e( 'Excluded user IDs:', 'vigilante' ); ?></label><br>
5783 <textarea id="vigilante-f-activity-log-excluded-users" name="activity_log[excluded_users]" rows="3" cols="25"><?php echo esc_textarea( implode( "\n", $options['excluded_users'] ?? array() ) ); ?></textarea>
5784 <p class="description"><?php esc_html_e( 'One user ID per line. Actions by these users will not be logged.', 'vigilante' ); ?></p>
5785 </div>
5786 <div>
5787 <label for="vigilante-f-activity-log-excluded-ips"><?php esc_html_e( 'Excluded IPs:', 'vigilante' ); ?></label><br>
5788 <textarea id="vigilante-f-activity-log-excluded-ips" name="activity_log[excluded_ips]" rows="3" cols="25"><?php echo esc_textarea( implode( "\n", $options['excluded_ips'] ?? array() ) ); ?></textarea>
5789 <p class="description"><?php esc_html_e( 'One IP per line. Requests from these IPs will not be logged.', 'vigilante' ); ?></p>
5790 </div>
5791 </div>
5792 </td>
5793 </tr>
5794 </table>
5795 </div>
5796
5797 <p class="submit vigilante-submit-buttons">
5798 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
5799 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
5800 </button>
5801 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
5802 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
5803 </button>
5804 </p>
5805 </form>
5806
5807 <?php
5808 // Audit Alerts — alerting layer on top of Security Audit. Its own
5809 // settings section and form, rendered right after the logging settings
5810 // so the tab reads as "log this, exclude that, and alert me about this".
5811 $alerts = $this->settings->get_section( 'audit_alerts' );
5812 $immediate = isset( $alerts['immediate'] ) ? $alerts['immediate'] : array();
5813 $threshold = isset( $alerts['threshold'] ) ? $alerts['threshold'] : array();
5814 $alert_severity = isset( $immediate['min_severity'] ) ? $immediate['min_severity'] : 'critical';
5815 $alert_window = isset( $threshold['window'] ) ? $threshold['window'] : '1h';
5816 $threshold_cats = isset( $threshold['categories'] ) ? (array) $threshold['categories'] : array();
5817 $cat_labels = Vigilante_Audit_Alerts::category_labels();
5818 ?>
5819 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="audit_alerts" <?php echo $is_disabled ? 'inert' : ''; ?>>
5820 <div id="vigilante-section-audit-alerts" class="vigilante-settings-section">
5821 <h2>
5822 <?php esc_html_e( 'Audit Alerts', 'vigilante' ); ?>
5823 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5824 </h2>
5825 <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>
5826
5827 <table class="form-table">
5828 <tr id="field-audit-alerts-immediate">
5829 <th scope="row"><?php esc_html_e( 'Immediate alerts', 'vigilante' ); ?></th>
5830 <td>
5831 <label>
5832 <input type="checkbox" name="audit_alerts[immediate][enabled]" value="1" <?php checked( ! empty( $immediate['enabled'] ) ); ?>>
5833 <?php esc_html_e( 'Email me as soon as a serious event is logged', 'vigilante' ); ?>
5834 </label>
5835 <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>
5836 </td>
5837 </tr>
5838 <tr>
5839 <th scope="row"><label for="vigilante-f-audit-alerts-immediate-min-severity"><?php esc_html_e( 'Alert on severity', 'vigilante' ); ?></label></th>
5840 <td>
5841 <select id="vigilante-f-audit-alerts-immediate-min-severity" name="audit_alerts[immediate][min_severity]">
5842 <option value="critical" <?php selected( $alert_severity, 'critical' ); ?>><?php esc_html_e( 'Critical only (recommended)', 'vigilante' ); ?></option>
5843 <option value="warning" <?php selected( $alert_severity, 'warning' ); ?>><?php esc_html_e( 'Warning and Critical', 'vigilante' ); ?></option>
5844 </select>
5845 <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>
5846 </td>
5847 </tr>
5848 <tr id="field-audit-alerts-threshold">
5849 <th scope="row"><?php esc_html_e( 'Threshold alerts', 'vigilante' ); ?></th>
5850 <td>
5851 <label>
5852 <input type="checkbox" name="audit_alerts[threshold][enabled]" value="1" <?php checked( ! empty( $threshold['enabled'] ) ); ?>>
5853 <?php esc_html_e( 'Email me when a category spikes within a time window', 'vigilante' ); ?>
5854 </label>
5855 <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>
5856 </td>
5857 </tr>
5858 <tr>
5859 <th scope="row"><label for="vigilante-f-audit-alerts-threshold-window"><?php esc_html_e( 'Time window', 'vigilante' ); ?></label></th>
5860 <td>
5861 <select id="vigilante-f-audit-alerts-threshold-window" name="audit_alerts[threshold][window]">
5862 <option value="30m" <?php selected( $alert_window, '30m' ); ?>><?php esc_html_e( '30 minutes', 'vigilante' ); ?></option>
5863 <option value="1h" <?php selected( $alert_window, '1h' ); ?>><?php esc_html_e( '1 hour', 'vigilante' ); ?></option>
5864 <option value="6h" <?php selected( $alert_window, '6h' ); ?>><?php esc_html_e( '6 hours', 'vigilante' ); ?></option>
5865 <option value="24h" <?php selected( $alert_window, '24h' ); ?>><?php esc_html_e( '24 hours', 'vigilante' ); ?></option>
5866 </select>
5867 <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>
5868 </td>
5869 </tr>
5870 <tr>
5871 <th scope="row"><?php esc_html_e( 'Thresholds per category', 'vigilante' ); ?></th>
5872 <td>
5873 <fieldset style="display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:8px 24px; max-width:760px;">
5874 <?php
5875 foreach ( $cat_labels as $cat_slug => $cat_label ) :
5876 $cat_value = isset( $threshold_cats[ $cat_slug ] ) ? (int) $threshold_cats[ $cat_slug ] : 0;
5877 ?>
5878 <label style="display:flex;align-items:center;gap:8px;justify-content:space-between;">
5879 <span><?php echo esc_html( $cat_label ); ?></span>
5880 <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">
5881 </label>
5882 <?php endforeach; ?>
5883 </fieldset>
5884 <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>
5885 </td>
5886 </tr>
5887 <tr>
5888 <th scope="row"><?php esc_html_e( "Don't repeat alerts", 'vigilante' ); ?></th>
5889 <td>
5890 <input type="number" id="vigilante-f-audit-alerts-cooldown-minutes" 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">
5891 <label for="vigilante-f-audit-alerts-cooldown-minutes"><?php esc_html_e( 'minutes', 'vigilante' ); ?></label>
5892 <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>
5893 </td>
5894 </tr>
5895
5896 <tr>
5897 <th scope="row"><?php esc_html_e( 'Recipients', 'vigilante' ); ?></th>
5898 <td>
5899 <p class="description" style="margin-top:0;">
5900 <?php
5901 printf(
5902 /* translators: %s: Link to notification settings */
5903 esc_html__( 'Alerts go to the recipients configured in %s.', 'vigilante' ),
5904 '<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>'
5905 );
5906 ?>
5907 </p>
5908 <p style="margin:8px 0 0;">
5909 <button type="button" class="button vigilante-test-email-btn" data-original-text="<?php esc_attr_e( 'Send test email', 'vigilante' ); ?>">
5910 <?php esc_html_e( 'Send test email', 'vigilante' ); ?>
5911 </button>
5912 <span class="vigilante-test-email-result" style="margin-left:8px;"></span>
5913 </p>
5914 <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>
5915 </td>
5916 </tr>
5917 </table>
5918 </div>
5919
5920 <p class="submit vigilante-submit-buttons">
5921 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
5922 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
5923 </button>
5924 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
5925 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
5926 </button>
5927 </p>
5928 </form>
5929
5930 <div id="vigilante-section-audit-recent" class="vigilante-settings-section">
5931 <h2><?php esc_html_e( 'Recent Activity', 'vigilante' ); ?></h2>
5932
5933 <?php
5934 $logs = $this->activity_log->get_logs( array( 'per_page' => 20 ) );
5935 $total_logs = $this->activity_log->get_logs_count();
5936
5937 // Label maps for translated display
5938 $type_labels = array(
5939 'login' => __( 'Login', 'vigilante' ),
5940 'user' => __( 'User', 'vigilante' ),
5941 'content' => __( 'Content', 'vigilante' ),
5942 'plugin' => __( 'Plugin', 'vigilante' ),
5943 'theme' => __( 'Theme', 'vigilante' ),
5944 'settings' => __( 'Settings', 'vigilante' ),
5945 'comment' => __( 'Comment', 'vigilante' ),
5946 'media' => __( 'Media', 'vigilante' ),
5947 'firewall' => __( 'Firewall', 'vigilante' ),
5948 'file' => __( 'File', 'vigilante' ),
5949 'security' => __( 'Security', 'vigilante' ),
5950 'system' => __( 'System', 'vigilante' ),
5951 );
5952 $severity_labels = array(
5953 'info' => __( 'Info', 'vigilante' ),
5954 'warning' => __( 'Warning', 'vigilante' ),
5955 'critical' => __( 'Critical', 'vigilante' ),
5956 );
5957
5958 $firewall_options = $this->settings->get_section( 'firewall' );
5959 $ip_whitelist = $firewall_options['ip_whitelist'] ?? array();
5960 $ip_blacklist = $firewall_options['ip_blacklist'] ?? array();
5961 $ua_whitelist = $firewall_options['ua_whitelist'] ?? array();
5962 $ua_blacklist = $firewall_options['ua_blacklist'] ?? array();
5963 ?>
5964
5965 <div class="vigilante-log-filters">
5966 <input type="text" id="vigilante-log-search" aria-label="<?php esc_attr_e( 'Search the activity log', 'vigilante' ); ?>" size="1" placeholder="<?php esc_attr_e( 'Search logs (min. 3 characters)...', 'vigilante' ); ?>" class="vigilante-log-search-input">
5967 <select id="vigilante-log-type-filter" aria-label="<?php esc_attr_e( 'Filter the log by event type', 'vigilante' ); ?>">
5968 <option value=""><?php esc_html_e( 'All Types', 'vigilante' ); ?></option>
5969 <option value="login"><?php esc_html_e( 'Login', 'vigilante' ); ?></option>
5970 <option value="user"><?php esc_html_e( 'User', 'vigilante' ); ?></option>
5971 <option value="content"><?php esc_html_e( 'Content', 'vigilante' ); ?></option>
5972 <option value="plugin"><?php esc_html_e( 'Plugin', 'vigilante' ); ?></option>
5973 <option value="theme"><?php esc_html_e( 'Theme', 'vigilante' ); ?></option>
5974 <option value="settings"><?php esc_html_e( 'Settings', 'vigilante' ); ?></option>
5975 <option value="comment"><?php esc_html_e( 'Comment', 'vigilante' ); ?></option>
5976 <option value="media"><?php esc_html_e( 'Media', 'vigilante' ); ?></option>
5977 <option value="firewall"><?php esc_html_e( 'Firewall', 'vigilante' ); ?></option>
5978 <option value="file"><?php esc_html_e( 'File', 'vigilante' ); ?></option>
5979 <option value="security"><?php esc_html_e( 'Security', 'vigilante' ); ?></option>
5980 <option value="system"><?php esc_html_e( 'System', 'vigilante' ); ?></option>
5981 </select>
5982 <select id="vigilante-log-severity-filter" aria-label="<?php esc_attr_e( 'Filter the log by severity', 'vigilante' ); ?>">
5983 <option value=""><?php esc_html_e( 'All Severities', 'vigilante' ); ?></option>
5984 <option value="info"><?php esc_html_e( 'Info', 'vigilante' ); ?></option>
5985 <option value="warning"><?php esc_html_e( 'Warning', 'vigilante' ); ?></option>
5986 <option value="critical"><?php esc_html_e( 'Critical', 'vigilante' ); ?></option>
5987 </select>
5988 <select id="vigilante-log-method-filter" aria-label="<?php esc_attr_e( 'Filter the log by HTTP method', 'vigilante' ); ?>">
5989 <option value=""><?php esc_html_e( 'All Methods', 'vigilante' ); ?></option>
5990 <option value="GET">GET</option>
5991 <option value="POST">POST</option>
5992 <option value="PUT">PUT</option>
5993 <option value="DELETE">DELETE</option>
5994 <option value="PATCH">PATCH</option>
5995 <option value="OPTIONS">OPTIONS</option>
5996 <option value="HEAD">HEAD</option>
5997 </select>
5998 <button type="button" id="vigilante-log-refresh" class="button"><?php esc_html_e( 'Refresh', 'vigilante' ); ?></button>
5999 <span class="vigilante-pagination" id="vigilante-log-pagination" data-total="<?php echo esc_attr( $total_logs ); ?>" data-per-page="20" data-page="1">
6000 <?php if ( $total_logs > 20 ) : ?>
6001 <button type="button" class="vigilante-page-first" title="<?php esc_attr_e( 'First page', 'vigilante' ); ?>" disabled>&laquo;</button>
6002 <button type="button" class="vigilante-page-prev" title="<?php esc_attr_e( 'Previous page', 'vigilante' ); ?>" disabled>&lsaquo;</button>
6003 <?php endif; ?>
6004 <span class="vigilante-page-info">
6005 <?php
6006 $showing = min( 20, $total_logs );
6007 printf(
6008 /* translators: 1: first item, 2: last item, 3: total items */
6009 esc_html__( '%1$d–%2$d of %3$d', 'vigilante' ),
6010 $total_logs > 0 ? 1 : 0,
6011 absint( $showing ),
6012 absint( $total_logs )
6013 );
6014 ?>
6015 </span>
6016 <?php if ( $total_logs > 20 ) : ?>
6017 <button type="button" class="vigilante-page-next" title="<?php esc_attr_e( 'Next page', 'vigilante' ); ?>">&rsaquo;</button>
6018 <button type="button" class="vigilante-page-last" title="<?php esc_attr_e( 'Last page', 'vigilante' ); ?>">&raquo;</button>
6019 <?php endif; ?>
6020 </span>
6021 </div>
6022
6023 <div class="vigilante-log-table-wrap">
6024 <table id="vigilante-activity-log-table" class="wp-list-table widefat striped">
6025 <thead>
6026 <tr>
6027 <th class="column-date"><?php esc_html_e( 'Date', 'vigilante' ); ?></th>
6028 <th class="column-type"><?php esc_html_e( 'Type', 'vigilante' ); ?></th>
6029 <th class="column-method"><?php esc_html_e( 'Method', 'vigilante' ); ?></th>
6030 <th class="column-severity"><?php esc_html_e( 'Severity', 'vigilante' ); ?></th>
6031 <th class="column-message"><?php esc_html_e( 'Message', 'vigilante' ); ?></th>
6032 <th class="column-user"><?php esc_html_e( 'User', 'vigilante' ); ?></th>
6033 <th class="column-ip"><?php esc_html_e( 'IP', 'vigilante' ); ?></th>
6034 <th class="column-details"><?php esc_html_e( 'Details', 'vigilante' ); ?></th>
6035 </tr>
6036 </thead>
6037 <tbody>
6038 <?php
6039 if ( empty( $logs ) ) :
6040 ?>
6041 <tr><td colspan="8"><?php esc_html_e( 'No log entries found.', 'vigilante' ); ?></td></tr>
6042 <?php else : ?>
6043 <?php foreach ( $logs as $log ) :
6044 $request_method = isset( $log->request_method ) ? $log->request_method : '';
6045 // Prepare details as a simple object
6046 $ip_val = (string) ( $log->ip_address ?? '' );
6047 $ua_val = (string) ( $log->user_agent ?? '' );
6048 $details = array(
6049 'id' => (int) $log->id,
6050 'type' => (string) ( $log->event_type ?? '' ),
6051 'action' => (string) ( $log->event_action ?? '' ),
6052 'message' => (string) ( $log->event_message ?? '' ),
6053 'user' => (string) ( $log->user_login ?? '' ),
6054 'ip' => $ip_val,
6055 'user_agent' => $ua_val,
6056 'request_method' => (string) $request_method,
6057 'request_uri' => Vigilante_Activity_Log::extract_request_uri( $log->extra_data ?? '' ),
6058 'date' => (string) ( $log->created_at ?? '' ),
6059 'severity' => (string) ( $log->severity ?? 'info' ),
6060 'is_ip_whitelisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_whitelist, true ) ),
6061 'is_ip_blacklisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_blacklist, true ) ),
6062 'is_ua_whitelisted' => ( '' !== $ua_val && in_array( $ua_val, $ua_whitelist, true ) ),
6063 'is_ua_blacklisted' => ( '' !== $ua_val && in_array( $ua_val, $ua_blacklist, true ) ),
6064 );
6065 $display_type = isset( $type_labels[ $log->event_type ] ) ? $type_labels[ $log->event_type ] : $log->event_type;
6066 $display_severity = isset( $severity_labels[ $log->severity ] ) ? $severity_labels[ $log->severity ] : $log->severity;
6067 ?>
6068 <tr class="vigilante-severity-<?php echo esc_attr( $log->severity ); ?>">
6069 <td><?php echo esc_html( $log->created_at ); ?></td>
6070 <td><?php echo esc_html( $display_type ); ?></td>
6071 <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>
6072 <td><span class="vigilante-badge vigilante-badge-<?php echo esc_attr( $log->severity ); ?>"><?php echo esc_html( $display_severity ); ?></span></td>
6073 <td><?php echo esc_html( $log->event_message ); ?></td>
6074 <td><?php echo esc_html( $log->user_login ?? '-' ); ?></td>
6075 <td><code><?php echo esc_html( $log->ip_address ); ?></code></td>
6076 <td>
6077 <button type="button" class="button button-small vigilante-view-log-details"
6078 data-details='<?php echo esc_attr( wp_json_encode( $details, JSON_HEX_APOS | JSON_HEX_QUOT ) ); ?>'>
6079 <?php esc_html_e( 'View', 'vigilante' ); ?>
6080 </button>
6081 </td>
6082 </tr>
6083 <?php endforeach; ?>
6084 <?php endif; ?>
6085 </tbody>
6086 </table>
6087 </div>
6088
6089 <!-- Log Details Modal -->
6090 <div id="vigilante-log-details-modal" class="vigilante-modal" style="display: none;">
6091 <div class="vigilante-modal-content">
6092 <span class="vigilante-modal-close">&times;</span>
6093 <h3><?php esc_html_e( 'Log Entry Details', 'vigilante' ); ?></h3>
6094 <div id="vigilante-log-details-content"></div>
6095 </div>
6096 </div>
6097
6098 <p>
6099 <button type="button" class="button vigilante-export-logs"><?php esc_html_e( 'Export Audit Log', 'vigilante' ); ?></button>
6100 <button type="button" class="button vigilante-clear-logs" style="color: #a00;"><?php esc_html_e( 'Clear All Logs', 'vigilante' ); ?></button>
6101 </p>
6102 </div>
6103 <?php
6104 }
6105
6106 /**
6107 * Render File Integrity tab
6108 */
6109 private function render_tab_file_integrity() {
6110 $is_disabled = $this->render_module_disabled_notice( 'file_integrity' );
6111 $options = $this->settings->get_section( 'file_integrity' );
6112 // On the main site of a network the critical-file scan is the network's
6113 // canary for a change to wp-config.php or the root .htaccess, so a
6114 // main-site admin without network rights cannot turn it off. Since
6115 // 2.11.8; see Vigilante_Settings::get_main_site_file_settings().
6116 $vg_main_locked = $this->main_site_files_locked();
6117 $last_scan = get_option( 'vigilante_last_integrity_scan' );
6118 $last_results = get_option( 'vigilante_last_integrity_results' );
6119 $ignored_files = get_option( 'vigilante_ignored_files', array() );
6120
6121 // Backward compat: convert old notify_on_changes to notify_level
6122 $notify_level = $options['notify_level'] ?? '';
6123 if ( empty( $notify_level ) ) {
6124 $notify_level = ! empty( $options['notify_on_changes'] ) ? 'all' : 'disabled';
6125 }
6126
6127 // Closed + Removed plugins data. Surfaced inside Last Scan Results so the
6128 // user sees file findings and plugin closures together (same tier of risk,
6129 // same UI), and as the trigger to keep Last Scan Results open even when no
6130 // file scan has run yet (the daily cron may have populated this section).
6131 //
6132 // Gating by last_check_time > 0 is how "Clear Previous Results" visually
6133 // resets this block: the option vigilante_plugin_status_last_check is
6134 // deleted on Clear, but the state map and the ignored list survive so the
6135 // next scan reconstructs without degrading a 'removed' slug. While
6136 // last_check is 0, we treat the plugin_status data as if it didn't exist.
6137 if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) {
6138 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
6139 }
6140 $closed_checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
6141 $closed_last_check = $closed_checker->get_last_check_time();
6142 if ( $closed_last_check > 0 ) {
6143 $closed_plugins = $closed_checker->get_closed_plugins();
6144 $ignored_closed_plugins = $closed_checker->get_ignored_closed_plugins();
6145 } else {
6146 $closed_plugins = array();
6147 $ignored_closed_plugins = array();
6148 }
6149 $has_closed = ! empty( $closed_plugins );
6150 $datetime_format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
6151 ?>
6152 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="file_integrity" <?php echo $is_disabled ? 'inert' : ''; ?>>
6153 <div id="vigilante-section-fi-monitoring" class="vigilante-settings-section">
6154 <h2>
6155 <?php esc_html_e( 'File Integrity Monitoring', 'vigilante' ); ?>
6156 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
6157 </h2>
6158 <p><?php esc_html_e( 'Detects file modifications using WordPress.org checksums.', 'vigilante' ); ?></p>
6159
6160 <table class="form-table">
6161 <tr>
6162 <th scope="row"><?php esc_html_e( 'Automatic Scans', 'vigilante' ); ?></th>
6163 <td>
6164 <label>
6165 <input type="checkbox" name="file_integrity[auto_scan]" value="1" <?php checked( ! empty( $options['auto_scan'] ) ); ?>>
6166 <?php esc_html_e( 'Enable scheduled file integrity scans', 'vigilante' ); ?>
6167 </label>
6168 </td>
6169 </tr>
6170 <tr>
6171 <th scope="row"><label for="vigilante-f-file-integrity-scan-frequency"><?php esc_html_e( 'Scan Frequency', 'vigilante' ); ?></label></th>
6172 <td>
6173 <select id="vigilante-f-file-integrity-scan-frequency" name="file_integrity[scan_frequency]">
6174 <option value="daily" <?php selected( $options['scan_frequency'] ?? 'daily', 'daily' ); ?>><?php esc_html_e( 'Daily', 'vigilante' ); ?></option>
6175 <option value="weekly" <?php selected( $options['scan_frequency'] ?? 'daily', 'weekly' ); ?>><?php esc_html_e( 'Weekly', 'vigilante' ); ?></option>
6176 </select>
6177 </td>
6178 </tr>
6179 <tr>
6180 <th scope="row"><label for="vigilante-f-file-integrity-notify-level"><?php esc_html_e( 'Email Notifications', 'vigilante' ); ?></label></th>
6181 <td>
6182 <select id="vigilante-f-file-integrity-notify-level" name="file_integrity[notify_level]">
6183 <option value="all" <?php selected( $notify_level, 'all' ); ?>><?php esc_html_e( 'All issues (modified + suspicious)', 'vigilante' ); ?></option>
6184 <option value="suspicious_only" <?php selected( $notify_level, 'suspicious_only' ); ?>><?php esc_html_e( 'Suspicious files only', 'vigilante' ); ?></option>
6185 <option value="disabled" <?php selected( $notify_level, 'disabled' ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option>
6186 </select>
6187 <p class="description"><?php esc_html_e( '"Suspicious files only" reduces noise by skipping modified file notifications. Recommended for most sites.', 'vigilante' ); ?></p>
6188 </td>
6189 </tr>
6190 <tr>
6191 <th scope="row"><?php esc_html_e( 'Instant Alert', 'vigilante' ); ?></th>
6192 <td>
6193 <label>
6194 <input type="checkbox" name="file_integrity[instant_alert]" value="1" <?php checked( ! empty( $options['instant_alert'] ) ); ?>>
6195 <?php esc_html_e( 'Send immediate alert when modified, suspicious or additional files are detected, or when a closed plugin is found', 'vigilante' ); ?>
6196 </label>
6197 <p class="description"><?php esc_html_e( 'Fires even if the Email Notifications setting above is set to Disabled.', 'vigilante' ); ?></p>
6198 <p class="description">
6199 <?php
6200 printf(
6201 /* translators: %s: Link to notification settings */
6202 esc_html__( '&#9432; Notifications are sent to the recipients configured in %s.', 'vigilante' ),
6203 '<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>'
6204 );
6205 ?>
6206 </p>
6207 </td>
6208 </tr>
6209 <tr>
6210 <th scope="row"><?php esc_html_e( 'Test email', 'vigilante' ); ?></th>
6211 <td>
6212 <button type="button" class="button vigilante-test-email-btn" data-original-text="<?php esc_attr_e( 'Send test email', 'vigilante' ); ?>">
6213 <?php esc_html_e( 'Send test email', 'vigilante' ); ?>
6214 </button>
6215 <span class="vigilante-test-email-result" style="margin-left:8px;"></span>
6216 <p class="description"><?php esc_html_e( 'Sends a test message to the configured recipients to confirm email delivery works.', 'vigilante' ); ?></p>
6217 </td>
6218 </tr>
6219 <tr>
6220 <th scope="row"><?php esc_html_e( 'Scan Scope', 'vigilante' ); ?></th>
6221 <td>
6222 <fieldset>
6223 <label>
6224 <input type="checkbox" name="file_integrity[scan_core]" value="1" <?php checked( $options['scan_core'] ?? true ); ?>>
6225 <?php esc_html_e( 'Core files (compare against WordPress.org checksums)', 'vigilante' ); ?>
6226 </label>
6227 <br>
6228 <label>
6229 <input type="checkbox" name="file_integrity[scan_plugins]" value="1" <?php checked( $options['scan_plugins'] ?? true ); ?>>
6230 <?php esc_html_e( 'Plugins (WordPress.org repository plugins)', 'vigilante' ); ?>
6231 </label>
6232 <br>
6233 <label>
6234 <input type="checkbox" name="file_integrity[scan_themes]" value="1" <?php checked( $options['scan_themes'] ?? true ); ?>>
6235 <?php esc_html_e( 'Themes (WordPress.org repository themes)', 'vigilante' ); ?>
6236 </label>
6237 <br>
6238 <label>
6239 <input type="checkbox" name="file_integrity[scan_uploads]" value="1" <?php checked( $options['scan_uploads'] ?? true ); ?>>
6240 <?php esc_html_e( 'Uploads directory (detect PHP files, double extensions, .htaccess)', 'vigilante' ); ?>
6241 </label>
6242 <br>
6243 <label>
6244 <input type="checkbox" name="file_integrity[scan_critical_config]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( $options['scan_critical_config'] ?? true ); ?>>
6245 <?php esc_html_e( 'Critical config files (wp-config.php, .htaccess baseline monitoring)', 'vigilante' ); ?>
6246 <?php if ( $vg_main_locked ) : ?>
6247 <span class="description" style="display:block;margin-left:24px;"><?php echo esc_html( Vigilante_Settings::get_shared_files_notice() ); ?></span>
6248 <?php endif; ?>
6249 </label>
6250 <br>
6251 <label>
6252 <input type="checkbox" name="file_integrity[check_closed_plugins]" value="1" <?php checked( $options['check_closed_plugins'] ?? true ); ?>>
6253 <?php esc_html_e( 'Closed plugins (daily check against the WordPress.org repository)', 'vigilante' ); ?>
6254 </label>
6255 </fieldset>
6256 </td>
6257 </tr>
6258 <tr>
6259 <th scope="row"><label for="vigilante-f-file-integrity-excluded-paths"><?php esc_html_e( 'Excluded Paths', 'vigilante' ); ?></label></th>
6260 <td>
6261 <textarea id="vigilante-f-file-integrity-excluded-paths" 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>
6262 <p class="description"><?php esc_html_e( 'One path per line, relative to the WordPress root. A path such as wp-content/cache excludes exactly that folder and everything under it. A name on its own, such as cache, excludes any folder called exactly that, wherever it is.', 'vigilante' ); ?></p>
6263 </td>
6264 </tr>
6265 <tr>
6266 <th scope="row"><label for="vigilante-f-file-integrity-excluded-extensions"><?php esc_html_e( 'Excluded Extensions', 'vigilante' ); ?></label></th>
6267 <td>
6268 <textarea id="vigilante-f-file-integrity-excluded-extensions" 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>
6269 <p class="description">
6270 <?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' ); ?>
6271 <br>
6272 <?php
6273 printf(
6274 /* translators: 1: opening <code>, 2: closing </code>. Placeholders wrap the scoped-extension example. */
6275 esc_html__( 'An extension on its own applies to the whole site. To limit it to one folder, write it as %1$swp-content/languages/*.json%2$s, which leaves the same extension watched everywhere else.', 'vigilante' ),
6276 '<code>',
6277 '</code>'
6278 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded.
6279 ?>
6280 </p>
6281 </td>
6282 </tr>
6283 </table>
6284 </div>
6285
6286 <p class="submit vigilante-submit-buttons">
6287 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
6288 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
6289 </button>
6290 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
6291 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
6292 </button>
6293 <span class="vigilante-buttons-separator"></span>
6294 <button type="button" class="button button-primary vigilante-run-scan">
6295 <?php esc_html_e( 'Run Scan Now', 'vigilante' ); ?>
6296 </button>
6297 <button type="button" class="button vigilante-clear-scan-btn vigilante-clear-scan">
6298 <?php esc_html_e( 'Clear Previous Results', 'vigilante' ); ?>
6299 </button>
6300 </p>
6301 </form>
6302
6303 <div id="vigilante-scan-results" class="vigilante-settings-section" style="display:none;"></div>
6304
6305 <?php if ( $last_scan || $has_closed || $closed_last_check > 0 ) : ?>
6306 <div id="vigilante-section-fi-last-scan" class="vigilante-settings-section">
6307 <h2><?php esc_html_e( 'Last Scan Results', 'vigilante' ); ?></h2>
6308 <?php if ( $last_scan ) : ?>
6309 <p>
6310 <?php
6311 // Build the "X files scanned" hint inline with the date so it doesn't
6312 // need its own stat box (keeps the row compact when closed plugins are
6313 // present).
6314 $scanned_total = 0;
6315 if ( $last_results ) {
6316 $scanned_total = (int) ( $last_results['ok'] ?? 0 )
6317 + count( $last_results['modified'] ?? array() )
6318 + count( $last_results['suspicious'] ?? array() )
6319 + count( $last_results['extra'] ?? array() )
6320 + count( $ignored_files );
6321 }
6322 if ( $scanned_total > 0 ) {
6323 printf(
6324 /* translators: 1: date and time of last scan, 2: formatted file count */
6325 esc_html__( 'Last scan: %1$s (%2$s files scanned)', 'vigilante' ),
6326 esc_html( wp_date( $datetime_format, $last_scan ) ),
6327 esc_html( number_format_i18n( $scanned_total ) )
6328 );
6329 } else {
6330 printf(
6331 /* translators: %s: date and time of last scan */
6332 esc_html__( 'Last scan: %s', 'vigilante' ),
6333 esc_html( wp_date( $datetime_format, $last_scan ) )
6334 );
6335 }
6336 if ( $closed_last_check > 0 && $closed_last_check !== (int) $last_scan ) {
6337 echo ' &middot; ';
6338 printf(
6339 /* translators: %s: date and time of last closed plugins check */
6340 esc_html__( 'Closed plugins last checked: %s', 'vigilante' ),
6341 esc_html( wp_date( $datetime_format, $closed_last_check ) )
6342 );
6343 }
6344 ?>
6345 </p>
6346 <?php elseif ( $closed_last_check > 0 ) : ?>
6347 <p>
6348 <?php
6349 printf(
6350 /* translators: %s: date and time of last closed plugins check */
6351 esc_html__( 'Closed plugins last checked: %s &middot; the daily cron is running, no full integrity scan yet.', 'vigilante' ),
6352 esc_html( wp_date( $datetime_format, $closed_last_check ) )
6353 );
6354 ?>
6355 </p>
6356 <?php endif; ?>
6357 <div id="vigilante-last-scan-results">
6358 <?php if ( $last_results || $has_closed ) : ?>
6359 <div class="vigilante-scan-summary">
6360 <?php if ( $last_results ) : ?>
6361 <div class="vigilante-scan-stat vigilante-stat-ok">
6362 <span class="vigilante-stat-number"><?php echo esc_html( $last_results['ok'] ?? 0 ); ?></span>
6363 <span class="vigilante-stat-label"><?php esc_html_e( 'OK', 'vigilante' ); ?></span>
6364 </div>
6365 <div class="vigilante-scan-stat vigilante-stat-modified">
6366 <span class="vigilante-stat-number"><?php echo esc_html( count( $last_results['modified'] ?? array() ) ); ?></span>
6367 <span class="vigilante-stat-label"><?php esc_html_e( 'Modified', 'vigilante' ); ?></span>
6368 </div>
6369 <div class="vigilante-scan-stat vigilante-stat-suspicious">
6370 <span class="vigilante-stat-number"><?php echo esc_html( count( $last_results['suspicious'] ?? array() ) ); ?></span>
6371 <span class="vigilante-stat-label"><?php esc_html_e( 'Suspicious', 'vigilante' ); ?></span>
6372 </div>
6373 <div class="vigilante-scan-stat vigilante-stat-extra">
6374 <span class="vigilante-stat-number"><?php echo esc_html( count( $last_results['extra'] ?? array() ) ); ?></span>
6375 <span class="vigilante-stat-label"><?php esc_html_e( 'Extra', 'vigilante' ); ?></span>
6376 </div>
6377 <?php endif; ?>
6378 <?php if ( $has_closed ) : ?>
6379 <div class="vigilante-scan-stat vigilante-stat-suspicious">
6380 <span class="vigilante-stat-number" style="color: #d63638;"><?php echo (int) count( $closed_plugins ); ?></span>
6381 <span class="vigilante-stat-label"><?php esc_html_e( 'Closed/Removed', 'vigilante' ); ?></span>
6382 </div>
6383 <?php endif; ?>
6384 <?php if ( ! empty( $ignored_files ) ) : ?>
6385 <div class="vigilante-scan-stat vigilante-stat-ignored">
6386 <span class="vigilante-stat-number"><?php echo esc_html( count( $ignored_files ) ); ?></span>
6387 <span class="vigilante-stat-label"><?php esc_html_e( 'Ignored', 'vigilante' ); ?></span>
6388 </div>
6389 <?php endif; ?>
6390 </div>
6391
6392 <?php if ( ! empty( $last_results['suspicious'] ) ) : ?>
6393 <div class="vigilante-file-list vigilante-suspicious-files vigilante-paginated-section" data-bulk-mode="ignore">
6394 <h3 style="color: #d63638;"><?php esc_html_e( 'Suspicious Files', 'vigilante' ); ?></h3>
6395 <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>
6396 <div class="vigilante-fi-bulk-bar">
6397 <button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button>
6398 <span class="vigilante-fi-bulk-count" aria-live="polite"></span>
6399 </div>
6400 <div class="vigilante-fi-pagination-wrap"></div>
6401 <table class="wp-list-table widefat fixed striped vigilante-fi-paginated">
6402 <thead>
6403 <tr>
6404 <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>
6405 <th><?php esc_html_e( 'File', 'vigilante' ); ?></th>
6406 <th style="width: 250px;"><?php esc_html_e( 'Reason', 'vigilante' ); ?></th>
6407 <th style="width: 120px;"><?php esc_html_e( 'Type', 'vigilante' ); ?></th>
6408 <th style="width: 80px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
6409 </tr>
6410 </thead>
6411 <tbody>
6412 <?php
6413 foreach ( $last_results['suspicious'] as $item ) {
6414 $file_path = '';
6415 $file_reason = __( 'Unknown', 'vigilante' );
6416 $file_type = 'unknown';
6417
6418 if ( is_array( $item ) ) {
6419 if ( isset( $item['file'] ) ) {
6420 $file_path = $item['file'];
6421 }
6422 if ( isset( $item['reason'] ) ) {
6423 $file_reason = $item['reason'];
6424 }
6425 if ( isset( $item['type'] ) ) {
6426 $file_type = $item['type'];
6427 }
6428 } else {
6429 $file_path = (string) $item;
6430 }
6431 ?>
6432 <tr>
6433 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th>
6434 <td><code style="color: #d63638;"><?php echo esc_html( $file_path ); ?></code></td>
6435 <td><?php echo esc_html( $file_reason ); ?></td>
6436 <td><?php echo esc_html( $file_type ); ?></td>
6437 <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>
6438 </tr>
6439 <?php
6440 }
6441 ?>
6442 </tbody>
6443 </table>
6444 </div>
6445 <?php endif; ?>
6446
6447 <?php if ( ! empty( $last_results['extra'] ) ) : ?>
6448 <div class="vigilante-file-list vigilante-extra-files vigilante-paginated-section" data-bulk-mode="ignore">
6449 <h3 style="color: #b32d2e;"><?php esc_html_e( 'Extra Files', 'vigilante' ); ?></h3>
6450 <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>
6451 <div class="vigilante-fi-bulk-bar">
6452 <button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button>
6453 <span class="vigilante-fi-bulk-count" aria-live="polite"></span>
6454 </div>
6455 <div class="vigilante-fi-pagination-wrap"></div>
6456 <table class="wp-list-table widefat fixed striped vigilante-fi-paginated">
6457 <thead>
6458 <tr>
6459 <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>
6460 <th><?php esc_html_e( 'File', 'vigilante' ); ?></th>
6461 <th style="width: 250px;"><?php esc_html_e( 'Reason', 'vigilante' ); ?></th>
6462 <th style="width: 120px;"><?php esc_html_e( 'Type', 'vigilante' ); ?></th>
6463 <th style="width: 80px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
6464 </tr>
6465 </thead>
6466 <tbody>
6467 <?php
6468 foreach ( $last_results['extra'] as $item ) {
6469 $file_path = is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item;
6470 $file_reason = is_array( $item ) ? ( $item['reason'] ?? __( 'Unknown', 'vigilante' ) ) : __( 'Unknown', 'vigilante' );
6471 $file_type = is_array( $item ) ? ( $item['type'] ?? 'unknown' ) : 'unknown';
6472 ?>
6473 <tr>
6474 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th>
6475 <td><code style="color: #b32d2e;"><?php echo esc_html( $file_path ); ?></code></td>
6476 <td><?php echo esc_html( $file_reason ); ?></td>
6477 <td><?php echo esc_html( $file_type ); ?></td>
6478 <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>
6479 </tr>
6480 <?php
6481 }
6482 ?>
6483 </tbody>
6484 </table>
6485 </div>
6486 <?php endif; ?>
6487
6488 <?php
6489 // Split critical config files from regular modified files.
6490 // Computed unconditionally so the three sub-sections that consume
6491 // these arrays (Critical Config, Closed + Removed, Modified Files)
6492 // can render independently and in the order the team picked.
6493 $critical_modified = array();
6494 $regular_modified = array();
6495 if ( $last_results && ! empty( $last_results['modified'] ) ) {
6496 foreach ( $last_results['modified'] as $item ) {
6497 if ( is_array( $item ) && isset( $item['type'] ) && 'critical_config' === $item['type'] ) {
6498 $critical_modified[] = $item;
6499 } else {
6500 $regular_modified[] = $item;
6501 }
6502 }
6503 }
6504 ?>
6505
6506 <?php if ( ! empty( $critical_modified ) ) : ?>
6507 <div class="vigilante-file-list vigilante-critical-config-files">
6508 <h3 style="color: #e36210;"><?php esc_html_e( 'Critical config files modified', 'vigilante' ); ?></h3>
6509 <p class="description">
6510 <?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' ); ?>
6511 </p>
6512 <table class="wp-list-table widefat fixed striped">
6513 <thead>
6514 <tr>
6515 <th><?php esc_html_e( 'File', 'vigilante' ); ?></th>
6516 <th style="width: 200px;"><?php esc_html_e( 'Changes', 'vigilante' ); ?></th>
6517 <th style="width: 220px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
6518 </tr>
6519 </thead>
6520 <tbody>
6521 <?php foreach ( $critical_modified as $crit_item ) :
6522 $crit_file = $crit_item['file'] ?? '';
6523 $crit_baseline_size = $crit_item['baseline_size'] ?? 0;
6524 $crit_current_size = $crit_item['current_size'] ?? 0;
6525 $crit_diff = $crit_item['diff'] ?? array();
6526 $crit_id = sanitize_html_class( $crit_file );
6527 $added_count = is_array( $crit_diff ) ? count( $crit_diff['added'] ?? array() ) : 0;
6528 $removed_count = is_array( $crit_diff ) ? count( $crit_diff['removed'] ?? array() ) : 0;
6529 // The lines of a shared file are for whoever approves it. Results
6530 // stored before 2.11.8 on the main site still carry them, so the
6531 // screen asks too, not only the scan that wrote them.
6532 $diff_network = ( is_array( $crit_diff ) && ! empty( $crit_diff['network'] ) ) || $this->critical_approval_locked();
6533 $diff_rescan = is_array( $crit_diff ) && ! empty( $crit_diff['rescan'] );
6534 $diff_redaction = is_array( $crit_diff ) && ! empty( $crit_diff['redaction'] );
6535 $diff_unavailable = $diff_network || ( is_array( $crit_diff ) && ! empty( $crit_diff['unavailable'] ) );
6536 ?>
6537 <tr>
6538 <td><code style="color: #e36210;"><?php echo esc_html( $crit_file ); ?></code></td>
6539 <td>
6540 <?php if ( ! $diff_unavailable ) : ?>
6541 <span style="color: #007017;">+<?php echo (int) $added_count; ?></span>
6542 <span style="color: #b32d2e;">-<?php echo (int) $removed_count; ?></span>
6543 <?php esc_html_e( 'lines', 'vigilante' ); ?><br>
6544 <?php endif; ?>
6545 <small style="color: #50575e;">
6546 <?php
6547 printf(
6548 /* translators: 1: baseline size, 2: current size */
6549 esc_html__( '%1$s &rarr; %2$s bytes', 'vigilante' ),
6550 esc_html( number_format_i18n( $crit_baseline_size ) ),
6551 esc_html( number_format_i18n( $crit_current_size ) )
6552 );
6553 ?>
6554 </small>
6555 </td>
6556 <td>
6557 <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' ); ?>">
6558 <?php esc_html_e( 'Review changes', 'vigilante' ); ?>
6559 </button>
6560 <?php if ( $this->critical_approval_locked() ) : ?>
6561 <span class="description" style="display:block;margin-top:4px;">
6562 <?php echo esc_html( $this->critical_approval_notice() ); ?>
6563 </span>
6564 <?php else : ?>
6565 <button type="button" class="button button-small button-primary vigilante-approve-critical-file" data-file="<?php echo esc_attr( $crit_file ); ?>">
6566 <?php esc_html_e( 'Approve', 'vigilante' ); ?>
6567 </button>
6568 <?php endif; ?>
6569 </td>
6570 </tr>
6571 <tr id="vigilante-critical-content-<?php echo esc_attr( $crit_id ); ?>" class="vigilante-critical-content-row" style="display:none;">
6572 <td colspan="3" style="padding: 0;">
6573 <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;">
6574 <?php if ( $diff_network ) : ?>
6575 <p style="color: #50575e; font-style: italic; margin: 0;">
6576 <?php esc_html_e( 'This file belongs to the whole network, so its line changes are only shown to network administrators, on the main site.', 'vigilante' ); ?>
6577 </p>
6578 <?php elseif ( $diff_rescan ) : ?>
6579 <p style="color: #50575e; font-style: italic; margin: 0;">
6580 <?php esc_html_e( 'Run a new scan to see the line changes of this file.', 'vigilante' ); ?>
6581 </p>
6582 <?php elseif ( $diff_redaction ) : ?>
6583 <p style="color: #50575e; font-style: italic; margin: 0;">
6584 <?php esc_html_e( 'The line changes of this file are not shown because a value in it could not be hidden safely. The change itself is still detected.', 'vigilante' ); ?>
6585 </p>
6586 <?php elseif ( $diff_unavailable ) : ?>
6587 <p style="color: #50575e; font-style: italic; margin: 0;">
6588 <?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' ); ?>
6589 </p>
6590 <?php elseif ( empty( $crit_diff['added'] ) && empty( $crit_diff['removed'] ) ) : ?>
6591 <p style="color: #50575e; font-style: italic; margin: 0;">
6592 <?php esc_html_e( 'No line-level changes detected (may be whitespace or reordering).', 'vigilante' ); ?>
6593 </p>
6594 <?php else : ?>
6595 <?php if ( ! empty( $crit_diff['removed'] ) ) : ?>
6596 <?php foreach ( $crit_diff['removed'] as $rline ) : ?>
6597 <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>
6598 <?php endforeach; ?>
6599 <?php endif; ?>
6600 <?php if ( ! empty( $crit_diff['added'] ) ) : ?>
6601 <?php foreach ( $crit_diff['added'] as $aline ) : ?>
6602 <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>
6603 <?php endforeach; ?>
6604 <?php endif; ?>
6605 <?php endif; ?>
6606 </div>
6607 </td>
6608 </tr>
6609 <?php endforeach; ?>
6610 </tbody>
6611 </table>
6612 </div>
6613 <?php endif; ?>
6614
6615 <?php if ( $has_closed ) : ?>
6616 <div class="vigilante-file-list vigilante-closed-plugins">
6617 <h3 id="vigilante-section-fi-closed-plugins" style="color: #d63638;"><?php esc_html_e( 'Closed + Removed Plugins', 'vigilante' ); ?></h3>
6618 <p class="description" style="color: #d63638;">
6619 <?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' ); ?>
6620 </p>
6621 <table class="wp-list-table widefat striped">
6622 <thead>
6623 <tr>
6624 <th><?php esc_html_e( 'Plugin', 'vigilante' ); ?></th>
6625 <th style="width: 70px;"><?php esc_html_e( 'Version', 'vigilante' ); ?></th>
6626 <th style="width: 90px;"><?php esc_html_e( 'State', 'vigilante' ); ?></th>
6627 <th style="width: 110px;"><?php esc_html_e( 'Closed date', 'vigilante' ); ?></th>
6628 <th><?php esc_html_e( 'Reason', 'vigilante' ); ?></th>
6629 <th style="width: 130px;"><?php esc_html_e( 'Detected', 'vigilante' ); ?></th>
6630 <th style="width: 90px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
6631 </tr>
6632 </thead>
6633 <tbody>
6634 <?php foreach ( $closed_plugins as $cp_slug => $cp_entry ) :
6635 $cp_state = $cp_entry['state'] ?? '';
6636 $cp_state_label = 'closed' === $cp_state ? __( 'Closed', 'vigilante' ) : __( 'Removed', 'vigilante' );
6637 $cp_state_color = 'closed' === $cp_state ? '#d63638' : '#b32d2e';
6638 $cp_reason = '';
6639 if ( ! empty( $cp_entry['closed_reason_text'] ) ) {
6640 $cp_reason = $cp_entry['closed_reason_text'];
6641 } elseif ( 'removed' === $cp_state ) {
6642 $cp_reason = __( 'Removed from repository (metadata hidden, typical of Security Issue closures)', 'vigilante' );
6643 }
6644 $cp_detected = isset( $cp_entry['first_detected'] ) ? (int) $cp_entry['first_detected'] : 0;
6645 ?>
6646 <tr>
6647 <td>
6648 <strong><?php echo esc_html( $cp_entry['name'] ?? $cp_slug ); ?></strong><br>
6649 <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>
6650 </td>
6651 <td><?php echo esc_html( $cp_entry['version'] ?? '' ); ?></td>
6652 <td><span style="color: <?php echo esc_attr( $cp_state_color ); ?>; font-weight: 600;"><?php echo esc_html( $cp_state_label ); ?></span></td>
6653 <td><?php echo esc_html( $cp_entry['closed_date'] ?? '' ); ?></td>
6654 <td><?php echo esc_html( $cp_reason ); ?></td>
6655 <td>
6656 <?php echo $cp_detected > 0 ? esc_html( wp_date( $datetime_format, $cp_detected ) ) : '&mdash;'; ?>
6657 </td>
6658 <td>
6659 <button type="button" class="button button-small vigilante-ignore-closed-plugin" data-slug="<?php echo esc_attr( $cp_slug ); ?>">
6660 <?php esc_html_e( 'Ignore', 'vigilante' ); ?>
6661 </button>
6662 </td>
6663 </tr>
6664 <?php endforeach; ?>
6665 </tbody>
6666 </table>
6667 </div>
6668 <?php endif; ?>
6669
6670 <?php if ( ! empty( $regular_modified ) ) : ?>
6671 <div class="vigilante-file-list vigilante-paginated-section" data-bulk-mode="ignore">
6672 <h3><?php esc_html_e( 'Modified Files', 'vigilante' ); ?></h3>
6673 <p class="description"><?php esc_html_e( 'These files (apparently) differ from the original WordPress or plugin versions.', 'vigilante' ); ?></p>
6674 <div class="vigilante-fi-bulk-bar">
6675 <button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button>
6676 <span class="vigilante-fi-bulk-count" aria-live="polite"></span>
6677 </div>
6678 <div class="vigilante-fi-pagination-wrap"></div>
6679 <table class="wp-list-table widefat fixed striped vigilante-fi-paginated">
6680 <thead>
6681 <tr>
6682 <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>
6683 <th><?php esc_html_e( 'File', 'vigilante' ); ?></th>
6684 <th style="width: 100px;"><?php esc_html_e( 'Type', 'vigilante' ); ?></th>
6685 <th style="width: 80px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
6686 </tr>
6687 </thead>
6688 <tbody>
6689 <?php
6690 foreach ( $regular_modified as $item ) {
6691 $file_path = '';
6692 $file_type = 'unknown';
6693
6694 if ( is_array( $item ) ) {
6695 if ( isset( $item['file'] ) ) {
6696 $file_path = $item['file'];
6697 }
6698 if ( isset( $item['type'] ) ) {
6699 $file_type = $item['type'];
6700 }
6701 } else {
6702 $file_path = (string) $item;
6703 }
6704 ?>
6705 <tr>
6706 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th>
6707 <td><code><?php echo esc_html( $file_path ); ?></code></td>
6708 <td><?php echo esc_html( $file_type ); ?></td>
6709 <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>
6710 </tr>
6711 <?php
6712 }
6713 ?>
6714 </tbody>
6715 </table>
6716 </div>
6717 <?php endif; ?>
6718
6719 <?php if ( $last_results && empty( $last_results['modified'] ) && empty( $last_results['suspicious'] ) && empty( $last_results['extra'] ) && ! $has_closed ) : ?>
6720 <p class="vigilante-all-clear" style="color: #00a32a; font-weight: bold;">
6721 <?php esc_html_e( 'Good Job! All files passed integrity check. No issues found.', 'vigilante' ); ?>
6722 </p>
6723 <?php endif; ?>
6724 <?php endif; ?>
6725 </div>
6726 </div>
6727 <?php endif; ?>
6728
6729 <?php if ( ! empty( $ignored_closed_plugins ) ) : ?>
6730 <div id="vigilante-section-fi-ignored-closed" class="vigilante-settings-section">
6731 <h2><?php esc_html_e( 'Ignored Closed + Removed Plugins', 'vigilante' ); ?></h2>
6732 <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>
6733 <table class="wp-list-table widefat fixed striped">
6734 <thead>
6735 <tr>
6736 <th><?php esc_html_e( 'Plugin', 'vigilante' ); ?></th>
6737 <th style="width: 110px;"><?php esc_html_e( 'State', 'vigilante' ); ?></th>
6738 <th style="width: 120px;"><?php esc_html_e( 'Closed date', 'vigilante' ); ?></th>
6739 <th style="width: 130px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
6740 </tr>
6741 </thead>
6742 <tbody>
6743 <?php foreach ( $ignored_closed_plugins as $icp_slug => $icp_entry ) :
6744 $icp_state = $icp_entry['state'] ?? '';
6745 $icp_state_label = 'closed' === $icp_state ? __( 'Closed', 'vigilante' ) : __( 'Removed', 'vigilante' );
6746 ?>
6747 <tr>
6748 <td>
6749 <strong><?php echo esc_html( $icp_entry['name'] ?? $icp_slug ); ?></strong><br>
6750 <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>
6751 </td>
6752 <td><?php echo esc_html( $icp_state_label ); ?></td>
6753 <td><?php echo esc_html( $icp_entry['closed_date'] ?? '' ); ?></td>
6754 <td>
6755 <button type="button" class="button button-small vigilante-unignore-closed-plugin" data-slug="<?php echo esc_attr( $icp_slug ); ?>">
6756 <?php esc_html_e( 'Stop ignoring', 'vigilante' ); ?>
6757 </button>
6758 </td>
6759 </tr>
6760 <?php endforeach; ?>
6761 </tbody>
6762 </table>
6763 <p style="margin-top: 10px;">
6764 <button type="button" class="button vigilante-clear-ignored-closed-plugins"><?php esc_html_e( 'Clear All Ignored Closed + Removed Plugins', 'vigilante' ); ?></button>
6765 </p>
6766 </div>
6767 <?php endif; ?>
6768
6769 <?php if ( ! empty( $ignored_files ) ) : ?>
6770 <div id="vigilante-section-fi-ignored" class="vigilante-settings-section">
6771 <h2><?php esc_html_e( 'Ignored Files', 'vigilante' ); ?></h2>
6772 <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>
6773 <div class="vigilante-paginated-section" data-bulk-mode="unignore">
6774 <div class="vigilante-fi-bulk-bar">
6775 <button type="button" class="button vigilante-bulk-unignore" disabled><?php esc_html_e( 'Stop ignoring selected', 'vigilante' ); ?></button>
6776 <span class="vigilante-fi-bulk-count" aria-live="polite"></span>
6777 </div>
6778 <div class="vigilante-fi-pagination-wrap"></div>
6779 <table class="wp-list-table widefat fixed striped vigilante-fi-paginated">
6780 <thead>
6781 <tr>
6782 <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>
6783 <th><?php esc_html_e( 'File', 'vigilante' ); ?></th>
6784 <th style="width: 120px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th>
6785 </tr>
6786 </thead>
6787 <tbody>
6788 <?php foreach ( $ignored_files as $file ) : ?>
6789 <tr>
6790 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file ); ?>"></th>
6791 <td><code><?php echo esc_html( $file ); ?></code></td>
6792 <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>
6793 </tr>
6794 <?php endforeach; ?>
6795 </tbody>
6796 </table>
6797 </div>
6798 <p style="margin-top: 10px;">
6799 <button type="button" class="button vigilante-clear-ignored"><?php esc_html_e( 'Clear All Ignored Files', 'vigilante' ); ?></button>
6800 </p>
6801 </div>
6802 <?php endif; ?>
6803 <?php
6804 }
6805
6806 /**
6807 * Render sidebar with promotional widgets
6808 */
6809 private function render_sidebar() {
6810 $promo_banner = new Vigilante_Promo_Banner( 'vigilante' );
6811 $promo_banner->render();
6812 }
6813
6814 /**
6815 * AJAX: Download a ZIP backup of the critical config files.
6816 *
6817 * Streams wp-config.php and .htaccess (and robots.txt if present) as a
6818 * downloadable archive. Config backups are no longer left as files under the
6819 * web root, so this hands the admin the archive directly.
6820 */
6821 public function ajax_download_files_backup() {
6822 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6823
6824 if ( ! current_user_can( 'manage_options' ) ) {
6825 wp_die( esc_html__( 'Permission denied.', 'vigilante' ), 403 );
6826 }
6827
6828 // The archive carries wp-config.php, which a whole network shares. On a
6829 // network manage_options is held by every subsite administrator, so the
6830 // same gate the writers use applies here.
6831 if ( ! Vigilante_Settings::can_write_shared_files() ) {
6832 wp_die( esc_html( Vigilante_Settings::get_shared_files_notice() ), 403 );
6833 }
6834
6835 $backup_manager = new Vigilante_Backup_Manager();
6836 $result = $backup_manager->stream_files_zip();
6837
6838 // stream_files_zip() exits on success; only a WP_Error returns here.
6839 if ( is_wp_error( $result ) ) {
6840 wp_die( esc_html( $result->get_error_message() ), 500 );
6841 }
6842 }
6843
6844 /**
6845 * AJAX: Save settings
6846 */
6847 public function ajax_save_settings() {
6848 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
6849
6850 if ( ! current_user_can( 'manage_options' ) ) {
6851 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6852 }
6853
6854 $section = isset( $_POST['section'] ) ? sanitize_key( $_POST['section'] ) : '';
6855
6856 // Handle $_POST['data'] based on type
6857 if ( isset( $_POST['data'] ) && is_array( $_POST['data'] ) ) {
6858 $data = map_deep( wp_unslash( $_POST['data'] ), 'sanitize_text_field' );
6859 } elseif ( isset( $_POST['data'] ) ) {
6860 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
6861 $raw_data = wp_unslash( $_POST['data'] );
6862 parse_str( $raw_data, $data );
6863 $data = map_deep( $data, 'sanitize_textarea_field' );
6864 } else {
6865 $data = array();
6866 }
6867
6868 if ( empty( $section ) ) {
6869 wp_send_json_error( __( 'Invalid section.', 'vigilante' ) );
6870 }
6871
6872 // Check if 2FA is being enabled or method changed (for notification sending)
6873 $send_2fa_notification = false;
6874 $send_login_url_notification = false;
6875
6876 if ( 'login_security' === $section ) {
6877 // Read old state from DB
6878 $db_options = get_option( Vigilante_Settings::OPTION_NAME, array() );
6879 $old_2fa_enabled = ! empty( $db_options['login_security']['two_factor']['enabled'] );
6880 $old_method = $db_options['login_security']['two_factor']['method'] ?? 'email';
6881
6882 // Check new state from submitted data
6883 $new_2fa_enabled = false;
6884 $notify_on_enable = false;
6885 $new_method = isset( $data['login_security']['two_factor']['method'] )
6886 ? sanitize_key( $data['login_security']['two_factor']['method'] )
6887 : 'email';
6888
6889 if ( isset( $data['login_security']['two_factor']['enabled'] ) ) {
6890 $new_2fa_enabled = filter_var( $data['login_security']['two_factor']['enabled'], FILTER_VALIDATE_BOOLEAN );
6891 }
6892 if ( isset( $data['login_security']['two_factor']['notify_on_enable'] ) ) {
6893 $notify_on_enable = filter_var( $data['login_security']['two_factor']['notify_on_enable'], FILTER_VALIDATE_BOOLEAN );
6894 }
6895
6896 // Send notification if:
6897 // 1. 2FA is being enabled (was off, now on) OR
6898 // 2. Method changed while 2FA is enabled
6899 if ( $notify_on_enable && $new_2fa_enabled ) {
6900 if ( ! $old_2fa_enabled || ( $old_2fa_enabled && $old_method !== $new_method ) ) {
6901 $send_2fa_notification = true;
6902 }
6903 }
6904
6905 // Check if login URL changed and notification is enabled
6906 $old_login_url = $db_options['login_security']['custom_login_url'] ?? '';
6907 $new_login_url = isset( $data['login_security']['custom_login_url'] )
6908 ? sanitize_title( $data['login_security']['custom_login_url'] )
6909 : '';
6910 $notify_on_url_change = isset( $data['login_security']['notify_on_login_url_change'] )
6911 ? filter_var( $data['login_security']['notify_on_login_url_change'], FILTER_VALIDATE_BOOLEAN )
6912 : false;
6913
6914 if ( $notify_on_url_change && ! empty( $new_login_url ) && $new_login_url !== $old_login_url ) {
6915 $send_login_url_notification = true;
6916 }
6917 }
6918
6919 // Get defaults
6920 $defaults = $this->settings->get_default_options();
6921
6922 // Read ONLY saved options from database (not merged with defaults)
6923 $saved_options = get_option( Vigilante_Settings::OPTION_NAME, array() );
6924
6925 // What is stored before this request changes anything: the shared file
6926 // settings this user may not change are put back from here (2.11.6).
6927 $stored_options = $saved_options;
6928 $locked = Vigilante_Settings::get_locked_file_settings();
6929
6930 if ( isset( $locked[ $section ] ) && true === $locked[ $section ] ) {
6931 wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
6932 }
6933
6934 // A module switch is a single key, so refusing says more than a success
6935 // that changed nothing, and the dashboard puts the toggle back.
6936 if ( 'modules' === $section && isset( $locked['modules'], $data['modules'] ) && is_array( $locked['modules'] ) && is_array( $data['modules'] ) ) {
6937 foreach ( array_keys( $data['modules'] ) as $vg_module ) {
6938 if ( in_array( sanitize_key( $vg_module ), $locked['modules'], true ) ) {
6939 wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
6940 }
6941 }
6942 }
6943
6944 $rejected_ips = array();
6945 $rejected_proxies = array();
6946
6947 // Handle modules
6948 if ( 'modules' === $section && isset( $data['modules'] ) ) {
6949 if ( ! isset( $saved_options['modules'] ) ) {
6950 $saved_options['modules'] = array();
6951 }
6952 foreach ( $data['modules'] as $module => $enabled ) {
6953 $module = sanitize_key( $module );
6954 $saved_options['modules'][ $module ] = in_array( $enabled, array( '1', 1, 'true', true ), true );
6955 }
6956 // Clear active preset when modules change
6957 update_option( 'vigilante_active_preset', '' );
6958 } else {
6959 // Process primary section
6960 if ( isset( $data[ $section ] ) && is_array( $data[ $section ] ) ) {
6961 $section_defaults = isset( $defaults[ $section ] ) ? $defaults[ $section ] : array();
6962 $current_section = isset( $saved_options[ $section ] ) ? $saved_options[ $section ] : array();
6963
6964 // Process the submitted data
6965 $processed = $this->process_section_data( $data[ $section ], $section_defaults, $current_section );
6966
6967 // The IP boxes are free text and, until 2.9.9, whatever was typed
6968 // went straight into the option. An entry the matcher can never
6969 // match still sits in a security list looking like protection,
6970 // so the ones that cannot match are dropped and reported back
6971 // instead of being stored in silence.
6972 $rejected_ips = $this->filter_ip_lists( $section, $processed, $rejected_proxies );
6973
6974 // Save the processed section
6975 $saved_options[ $section ] = $processed;
6976
6977 // Clear active preset when any section settings change
6978 update_option( 'vigilante_active_preset', '' );
6979 }
6980 }
6981
6982 // Clear cache before saving
6983 wp_cache_delete( Vigilante_Settings::OPTION_NAME, 'options' );
6984
6985 $saved_options = Vigilante_Settings::keep_locked_file_settings( $saved_options, $stored_options );
6986
6987 // Save to database
6988 update_option( Vigilante_Settings::OPTION_NAME, $saved_options );
6989
6990 // Clear the settings cache
6991 $this->settings->clear_cache();
6992
6993 // Apply changes based on section
6994 $this->apply_section_changes( $section, $saved_options );
6995
6996 // Send 2FA notifications after settings are saved
6997 $notification_result = null;
6998 if ( $send_2fa_notification ) {
6999 $notification_result = $this->send_2fa_enable_notifications();
7000 }
7001
7002 // Send login URL notifications after settings are saved
7003 $login_url_result = null;
7004 if ( $send_login_url_notification ) {
7005 $login_url_result = $this->send_login_url_notifications();
7006 }
7007
7008 // Build success message
7009 $message = __( 'Settings saved successfully.', 'vigilante' );
7010
7011 if ( $notification_result && $notification_result['sent'] > 0 ) {
7012 $message .= ' ' . sprintf(
7013 /* translators: %d: Number of emails sent */
7014 _n(
7015 '2FA notification sent to %d user.',
7016 '2FA notifications sent to %d users.',
7017 $notification_result['sent'],
7018 'vigilante'
7019 ),
7020 $notification_result['sent']
7021 );
7022 }
7023
7024 if ( $login_url_result && $login_url_result['sent'] > 0 ) {
7025 $message .= ' ' . sprintf(
7026 /* translators: %d: Number of emails sent */
7027 _n(
7028 'Login URL notification sent to %d user.',
7029 'Login URL notifications sent to %d users.',
7030 $login_url_result['sent'],
7031 'vigilante'
7032 ),
7033 $login_url_result['sent']
7034 );
7035 }
7036
7037 if ( ! empty( $rejected_ips ) ) {
7038 $message .= ' ' . sprintf(
7039 /* translators: %s: comma separated list of the entries that were not saved. */
7040 _n(
7041 'This entry is not a valid IP, CIDR range or wildcard, so it was not saved: %s',
7042 'These entries are not valid IPs, CIDR ranges or wildcards, so they were not saved: %s',
7043 count( $rejected_ips ),
7044 'vigilante'
7045 ),
7046 implode( ', ', array_map( 'esc_html', $rejected_ips ) )
7047 );
7048 }
7049
7050 if ( ! empty( $rejected_proxies ) ) {
7051 $message .= ' ' . sprintf(
7052 /* translators: %s: comma separated list of the trusted proxy entries that were not saved. */
7053 _n(
7054 'A trusted proxy must be an exact IP or a CIDR range, not a wildcard, so this entry was not saved: %s',
7055 'A trusted proxy must be an exact IP or a CIDR range, not a wildcard, so these entries were not saved: %s',
7056 count( $rejected_proxies ),
7057 'vigilante'
7058 ),
7059 implode( ', ', array_map( 'esc_html', $rejected_proxies ) )
7060 );
7061 }
7062
7063 wp_send_json_success( $message );
7064 }
7065
7066 /**
7067 * Keep only the IP patterns the matcher can actually match
7068 *
7069 * @since 2.9.9
7070 *
7071 * @param string $section Section being saved.
7072 * @param array $processed Section data, edited in place.
7073 * @return array Entries that were dropped, for the message back to the user.
7074 */
7075 private function filter_ip_lists( $section, &$processed, &$rejected_proxies = array() ) {
7076 $rejected_proxies = array();
7077
7078 // Trusted proxies feed an identity decision, so only exact addresses and
7079 // CIDR ranges belong there: a wildcard is stripped with its own message,
7080 // never stored looking effective. The matcher ignores it anyway (see
7081 // Vigilante_IP_Utils::in_list_ip_or_cidr), this stops it persisting.
7082 if ( 'firewall' === $section && isset( $processed['trusted_proxies'] ) && is_array( $processed['trusted_proxies'] ) ) {
7083 $split = Vigilante_IP_Utils::split_list_ip_or_cidr( $processed['trusted_proxies'] );
7084 $processed['trusted_proxies'] = $split['valid'];
7085 $rejected_proxies = $split['rejected'];
7086 }
7087
7088 $lists = array(
7089 'firewall' => array( 'ip_whitelist', 'ip_blacklist' ),
7090 'login_security' => array( 'ip_whitelist' ),
7091 );
7092
7093 if ( ! isset( $lists[ $section ] ) ) {
7094 return array();
7095 }
7096
7097 $rejected = array();
7098
7099 foreach ( $lists[ $section ] as $key ) {
7100 if ( ! isset( $processed[ $key ] ) || ! is_array( $processed[ $key ] ) ) {
7101 continue;
7102 }
7103
7104 $split = Vigilante_IP_Utils::split_list( $processed[ $key ] );
7105 $processed[ $key ] = $split['valid'];
7106 $rejected = array_merge( $rejected, $split['rejected'] );
7107 }
7108
7109 return array_values( array_unique( $rejected ) );
7110 }
7111
7112 /**
7113 * Send 2FA enable notifications to users
7114 *
7115 * @return array Result with 'sent' and 'failed' counts.
7116 */
7117 private function send_2fa_enable_notifications() {
7118 $result = array(
7119 'sent' => 0,
7120 'failed' => 0,
7121 );
7122
7123 // Get settings for roles and method
7124 $login_security = $this->settings->get_section( 'login_security' );
7125 $two_factor = isset( $login_security['two_factor'] ) ? $login_security['two_factor'] : array();
7126 $roles = isset( $two_factor['enforced_roles'] ) ? $two_factor['enforced_roles'] : array( 'administrator' );
7127 $method = isset( $two_factor['method'] ) ? $two_factor['method'] : 'email';
7128
7129 if ( empty( $roles ) ) {
7130 $roles = array( 'administrator' );
7131 }
7132
7133 $excluded = isset( $two_factor['excluded_users'] ) ? array_map( 'absint', $two_factor['excluded_users'] ) : array();
7134
7135 // Get users with these roles
7136 $args = array(
7137 'role__in' => $roles,
7138 );
7139 if ( ! empty( $excluded ) ) {
7140 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Small excluded users list from settings.
7141 $args['exclude'] = $excluded;
7142 }
7143 $users = get_users( $args );
7144
7145 if ( empty( $users ) ) {
7146 return $result;
7147 }
7148
7149 $site_name = get_bloginfo( 'name' );
7150 $from_name = ! empty( $two_factor['email_from_name'] ) ? $two_factor['email_from_name'] : $site_name;
7151
7152 if ( 'totp' === $method ) {
7153 // Use TOTP class for styled HTML emails
7154 if ( ! class_exists( 'Vigilante_Two_Factor_TOTP' ) ) {
7155 require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-totp.php';
7156 }
7157 $totp = new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log );
7158
7159 foreach ( $users as $user ) {
7160 // Skip users who already have TOTP configured
7161 $totp_data = $this->database->get_totp_data( $user->ID );
7162 if ( $totp_data && ! empty( $totp_data['is_configured'] ) ) {
7163 continue;
7164 }
7165
7166 $sent = $totp->send_activation_email( $user, $site_name, $from_name );
7167
7168 if ( $sent ) {
7169 $this->database->mark_2fa_notified( $user->ID );
7170 $result['sent']++;
7171 } else {
7172 $result['failed']++;
7173 }
7174 }
7175 } else {
7176 // Email method - use existing email 2FA class
7177 if ( ! class_exists( 'Vigilante_Two_Factor_Email' ) ) {
7178 require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-email.php';
7179 }
7180 $email_2fa = new Vigilante_Two_Factor_Email( $this->settings, $this->database, $this->activity_log );
7181 return $email_2fa->send_activation_notifications( false );
7182 }
7183
7184 return $result;
7185 }
7186
7187 /**
7188 * Send login URL change notifications to users with admin access
7189 *
7190 * @return array Result with 'sent' and 'failed' counts.
7191 */
7192 private function send_login_url_notifications() {
7193 $login_options = $this->settings->get_section( 'login_security' );
7194 $custom_url = ! empty( $login_options['custom_login_url'] ) ? sanitize_title( $login_options['custom_login_url'] ) : '';
7195
7196 if ( empty( $custom_url ) ) {
7197 return array( 'sent' => 0, 'failed' => 0 );
7198 }
7199
7200 $login_url = home_url( $custom_url . '/' );
7201 $site_name = get_bloginfo( 'name' );
7202
7203 $admin_roles = array( 'administrator', 'editor', 'author', 'contributor' );
7204 $users = get_users( array( 'role__in' => $admin_roles ) );
7205
7206 if ( empty( $users ) ) {
7207 return array( 'sent' => 0, 'failed' => 0 );
7208 }
7209
7210 $subject = sprintf(
7211 /* translators: %s: Site name */
7212 __( '[%s] Your login URL has changed', 'vigilante' ),
7213 $site_name
7214 );
7215
7216 $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' ) );
7217 $body .= Vigilante_Email_Template::url_box( $login_url, __( 'Your new login URL:', 'vigilante' ) );
7218 $body .= Vigilante_Email_Template::alert_box( __( 'The old login address (wp-login.php) will no longer work.', 'vigilante' ) );
7219 $body .= Vigilante_Email_Template::button( $login_url, __( 'Go to login', 'vigilante' ) );
7220
7221 $sent = 0;
7222 $failed = 0;
7223
7224 foreach ( $users as $user ) {
7225 $result = Vigilante_Email_Template::send(
7226 $user->user_email,
7227 $subject,
7228 __( 'Login URL changed', 'vigilante' ),
7229 $body
7230 );
7231 if ( $result ) {
7232 $sent++;
7233 } else {
7234 $failed++;
7235 }
7236 }
7237
7238 if ( $this->activity_log ) {
7239 $this->activity_log->log(
7240 'login',
7241 'login_url_notified',
7242 sprintf(
7243 /* translators: 1: Sent count, 2: Failed count */
7244 __( 'Login URL notification sent on save: %1$d sent, %2$d failed', 'vigilante' ),
7245 $sent,
7246 $failed
7247 )
7248 );
7249 }
7250
7251 return array( 'sent' => $sent, 'failed' => $failed );
7252 }
7253
7254 /**
7255 * Process section data maintaining proper types from defaults
7256 *
7257 * @param array $submitted_data Data submitted from form.
7258 * @param array $defaults Default values for this section.
7259 * @param array $current Current saved values.
7260 * @param string $section_name Section name for special handling.
7261 * @return array Processed data.
7262 */
7263 private function process_section_data( $submitted_data, $defaults, $current, $section_name = '' ) {
7264 // Start with defaults, then merge current saved values
7265 $result = array_replace_recursive( $defaults, $current );
7266
7267 // Process each submitted value
7268 foreach ( $submitted_data as $key => $value ) {
7269 $key = sanitize_key( $key );
7270
7271 if ( is_array( $value ) ) {
7272 // Nested array (like rate_limiting)
7273 $nested_defaults = isset( $defaults[ $key ] ) && is_array( $defaults[ $key ] ) ? $defaults[ $key ] : array();
7274 $nested_current = isset( $result[ $key ] ) && is_array( $result[ $key ] ) ? $result[ $key ] : array();
7275 $result[ $key ] = $this->process_section_data( $value, $nested_defaults, $nested_current, $key );
7276 } else {
7277 // Determine type from default value
7278 $default_value = isset( $defaults[ $key ] ) ? $defaults[ $key ] : null;
7279
7280 if ( null === $default_value ) {
7281 // No default, check current value type or use as string
7282 $current_value = isset( $current[ $key ] ) ? $current[ $key ] : null;
7283 if ( is_bool( $current_value ) ) {
7284 $result[ $key ] = in_array( $value, array( '1', 1, 'true', true ), true );
7285 } elseif ( is_int( $current_value ) ) {
7286 $result[ $key ] = intval( $value );
7287 } elseif ( is_array( $current_value ) ) {
7288 $result[ $key ] = is_string( $value ) ? array_filter( array_map( 'trim', explode( "\n", $value ) ) ) : (array) $value;
7289 } else {
7290 $result[ $key ] = sanitize_text_field( $value );
7291 }
7292 } elseif ( is_bool( $default_value ) ) {
7293 // Boolean: '1', 1, 'true' become true; '0', 0, '', 'false' become false
7294 $result[ $key ] = in_array( $value, array( '1', 1, 'true', true ), true );
7295 } elseif ( is_int( $default_value ) ) {
7296 // Integer - with special handling for time fields shown in minutes
7297 $int_value = intval( $value );
7298
7299 // Convert minutes to seconds for login_security duration fields
7300 // These are displayed as minutes in the form but stored as seconds
7301 if ( in_array( $key, array( 'lockout_duration', 'max_lockout_duration' ), true ) ) {
7302 $int_value = $int_value * 60;
7303 }
7304
7305 $result[ $key ] = $int_value;
7306 } elseif ( is_array( $default_value ) ) {
7307 // Array from textarea (e.g., IP lists)
7308 if ( is_string( $value ) ) {
7309 $result[ $key ] = array_filter( array_map( 'trim', explode( "\n", $value ) ) );
7310 } else {
7311 $result[ $key ] = (array) $value;
7312 }
7313 } else {
7314 // String - preserve newlines for textarea fields
7315 if ( is_string( $value ) && ( strpos( $value, "\n" ) !== false || strpos( $value, "\r" ) !== false ) ) {
7316 $result[ $key ] = sanitize_textarea_field( $value );
7317 } else {
7318 $result[ $key ] = sanitize_text_field( $value );
7319 }
7320 }
7321 }
7322 }
7323
7324 // Handle unchecked checkboxes: HTML forms don't submit unchecked boxes
7325 // If a boolean field exists in defaults but NOT in submitted_data, set it to false
7326 //
7327 // EXCEPTION: the top-level 'enabled' flag of every section is the
7328 // module's master switch and is controlled by the Dashboard module
7329 // toggle, NOT by a checkbox inside the section's form. Treating it
7330 // like a regular checkbox here would silently switch the module off
7331 // every time the user saves the tab — see the REST API enabled=false
7332 // regression. We only skip it at the top level (when section_name is
7333 // empty); nested 'enabled' fields like security_headers.csp.enabled
7334 // are real checkboxes and must keep the auto-unset behaviour.
7335 $preserve_top_level = array( 'enabled' );
7336
7337 foreach ( $defaults as $key => $default_value ) {
7338 if ( '' === $section_name && in_array( $key, $preserve_top_level, true ) ) {
7339 continue;
7340 }
7341 if ( is_bool( $default_value ) && ! array_key_exists( $key, $submitted_data ) ) {
7342 $result[ $key ] = false;
7343 } elseif ( is_array( $default_value ) && ! isset( $submitted_data[ $key ] ) ) {
7344 // Check if this is a flat value list (like excluded_users, enforced_roles, ip_whitelist)
7345 // vs a nested settings group (like rate_limiting, two_factor)
7346 // Flat lists: default is empty array OR all values are scalar
7347 $is_value_list = empty( $default_value );
7348 if ( ! $is_value_list ) {
7349 $is_value_list = true;
7350 foreach ( $default_value as $dv ) {
7351 if ( ! is_scalar( $dv ) ) {
7352 $is_value_list = false;
7353 break;
7354 }
7355 }
7356 }
7357
7358 if ( $is_value_list ) {
7359 // All items removed - reset to empty array
7360 $result[ $key ] = array();
7361 } else {
7362 // Nested settings group - handle boolean children
7363 foreach ( $default_value as $nested_key => $nested_default ) {
7364 if ( is_bool( $nested_default ) && isset( $result[ $key ] ) && is_array( $result[ $key ] ) ) {
7365 $nested_submitted = isset( $submitted_data[ $key ] ) && is_array( $submitted_data[ $key ] )
7366 ? $submitted_data[ $key ]
7367 : array();
7368 if ( ! array_key_exists( $nested_key, $nested_submitted ) ) {
7369 $result[ $key ][ $nested_key ] = false;
7370 }
7371 }
7372 }
7373 }
7374 }
7375 }
7376
7377 return $result;
7378 }
7379
7380 /**
7381 * AJAX: Export settings
7382 */
7383 public function ajax_export_settings() {
7384 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7385
7386 if ( ! current_user_can( 'manage_options' ) ) {
7387 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7388 }
7389
7390 $options = $this->settings->get_all_options();
7391 $filename = 'vigilante-settings-' . gmdate( 'Y-m-d-His' ) . '.json';
7392
7393 wp_send_json_success( array(
7394 'content' => wp_json_encode( $options, JSON_PRETTY_PRINT ),
7395 'filename' => $filename,
7396 ) );
7397 }
7398
7399 /**
7400 * AJAX: Import settings
7401 */
7402 public function ajax_import_settings() {
7403 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7404
7405 if ( ! current_user_can( 'manage_options' ) ) {
7406 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7407 }
7408
7409 // Accept both 'settings' (from JS) and 'content' (legacy).
7410 // Do NOT run sanitize_text_field() on the raw payload: it calls
7411 // wp_strip_all_tags() internally, which removes any "<...>" substring
7412 // and turns a valid export JSON into garbage if any stored value
7413 // contains < or > (htaccess snippets, email templates, etc.). The
7414 // real sanitization happens after json_decode(), via map_deep() on
7415 // the parsed array.
7416 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
7417 $content = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : '';
7418 if ( empty( $content ) ) {
7419 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
7420 $content = isset( $_POST['content'] ) ? wp_unslash( $_POST['content'] ) : '';
7421 }
7422
7423 if ( empty( $content ) || ! is_string( $content ) ) {
7424 wp_send_json_error( __( 'No content provided.', 'vigilante' ) );
7425 }
7426
7427 $imported = json_decode( $content, true );
7428
7429 if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $imported ) ) {
7430 wp_send_json_error( __( 'Invalid JSON format.', 'vigilante' ) );
7431 }
7432
7433 // Sanitize imported data recursively
7434 $imported = map_deep( $imported, 'sanitize_text_field' );
7435
7436 // Validate structure: only sections and keys of the schema survive, and
7437 // every value takes the type of its default. Until 2.11.0 this was an
7438 // array_replace_recursive() of the file over the defaults, so any key in
7439 // the file, known or not, landed in vigilante_options (S7). Sections
7440 // the file does not carry keep their defaults; a section it does carry
7441 // replaces the default one whole, because validate_options() has
7442 // already filled in whatever the file left out.
7443 $defaults = $this->settings->get_default_options();
7444 $validated = $this->settings->validate_options( $imported );
7445 $merged = $defaults;
7446
7447 foreach ( $validated as $section => $data ) {
7448 if ( is_array( $data ) ) {
7449 $merged[ $section ] = $data;
7450 }
7451 }
7452
7453 // Save
7454 $merged = Vigilante_Settings::keep_locked_file_settings( $merged, get_option( Vigilante_Settings::OPTION_NAME, array() ) );
7455 update_option( Vigilante_Settings::OPTION_NAME, $merged );
7456 $this->settings->clear_cache();
7457
7458 // Re-evaluate the active preset marker. The imported config may match
7459 // a known preset exactly, partially, or not at all — without this step
7460 // the dashboard would keep showing whatever preset was active before
7461 // the import even if the new config no longer matches it.
7462 $matched_preset = $this->detect_matching_preset( $merged );
7463 if ( null === $matched_preset ) {
7464 delete_option( 'vigilante_active_preset' );
7465 } else {
7466 update_option( 'vigilante_active_preset', $matched_preset );
7467 }
7468
7469 // Apply file changes after import
7470 $this->apply_all_file_changes( $merged );
7471
7472 // Refresh the Security Analyzer score so the dashboard widget reflects
7473 // the imported config rather than the pre-import scan. Reuse the
7474 // post-Under-Attack scan hook (same job: full scan, async).
7475 if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) {
7476 wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' );
7477 }
7478
7479 wp_send_json_success( __( 'Settings imported successfully.', 'vigilante' ) . $this->locked_file_settings_message() );
7480 }
7481
7482 /**
7483 * Detect whether a vigilante_options array matches a known preset.
7484 *
7485 * A preset matches when every field the preset explicitly declares is
7486 * present in the config with the same (normalised) value. Fields outside
7487 * the preset are ignored — they may have been modified by the user before
7488 * applying the preset and do not invalidate the match. This mirrors how
7489 * apply_preset() now layers presets on top of the user's existing config.
7490 *
7491 * @param array $options The current/imported vigilante_options.
7492 * @return string|null Preset id ('standard', 'maximum') or null if custom.
7493 */
7494 private function detect_matching_preset( $options ) {
7495 if ( ! is_array( $options ) ) {
7496 return null;
7497 }
7498
7499 $presets = $this->settings->get_presets();
7500
7501 foreach ( $presets as $preset_id => $preset_data ) {
7502 unset( $preset_data['name'], $preset_data['description'] );
7503 if ( $this->preset_subset_matches( $preset_data, $options ) ) {
7504 return $preset_id;
7505 }
7506 }
7507
7508 return null;
7509 }
7510
7511 /**
7512 * Check whether every leaf value inside $preset_subset exists with the
7513 * same (normalised) value at the same path inside $config.
7514 *
7515 * @param mixed $preset_subset Branch of the preset definition.
7516 * @param mixed $config Same branch in the live/imported config.
7517 * @return bool
7518 */
7519 private function preset_subset_matches( $preset_subset, $config ) {
7520 if ( is_array( $preset_subset ) ) {
7521 if ( ! is_array( $config ) ) {
7522 return false;
7523 }
7524 foreach ( $preset_subset as $key => $value ) {
7525 if ( ! array_key_exists( $key, $config ) ) {
7526 return false;
7527 }
7528 if ( ! $this->preset_subset_matches( $value, $config[ $key ] ) ) {
7529 return false;
7530 }
7531 }
7532 return true;
7533 }
7534
7535 return $this->normalise_scalar_for_compare( $preset_subset ) === $this->normalise_scalar_for_compare( $config );
7536 }
7537
7538 /**
7539 * Normalise a scalar value so that the variants WordPress and the form
7540 * layer routinely produce ('1' / 1 / true → "1"; '' / '0' / 0 / false /
7541 * null → "") compare equal. Other values become strings unchanged.
7542 *
7543 * @param mixed $value
7544 * @return string
7545 */
7546 private function normalise_scalar_for_compare( $value ) {
7547 if ( is_bool( $value ) ) {
7548 return $value ? '1' : '';
7549 }
7550 if ( null === $value ) {
7551 return '';
7552 }
7553 if ( is_int( $value ) || is_float( $value ) ) {
7554 return (string) $value;
7555 }
7556 if ( is_string( $value ) ) {
7557 if ( 'true' === $value ) {
7558 return '1';
7559 }
7560 if ( 'false' === $value ) {
7561 return '';
7562 }
7563 return $value;
7564 }
7565 // Arrays and objects shouldn't reach here (handled by recursion above),
7566 // but if they do, fall back to a stable comparable representation.
7567 return wp_json_encode( $value );
7568 }
7569
7570 /**
7571 * AJAX: Apply preset
7572 */
7573 public function ajax_apply_preset() {
7574 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7575
7576 if ( ! current_user_can( 'manage_options' ) ) {
7577 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7578 }
7579
7580 $preset = isset( $_POST['preset'] ) ? sanitize_key( $_POST['preset'] ) : '';
7581
7582 // Handle reset to defaults
7583 if ( 'reset' === $preset ) {
7584 $stored_options = get_option( Vigilante_Settings::OPTION_NAME, array() );
7585 $defaults = Vigilante_Settings::get_defaults_preserving_user_data( $stored_options );
7586 $defaults = Vigilante_Settings::keep_locked_file_settings( $defaults, $stored_options );
7587 update_option( Vigilante_Settings::OPTION_NAME, $defaults );
7588 $this->settings->clear_cache();
7589
7590 // Clear active preset
7591 update_option( 'vigilante_active_preset', '' );
7592
7593 // Apply file changes after reset
7594 $this->apply_all_file_changes( $defaults );
7595
7596 wp_send_json_success( __( 'Settings reset to defaults.', 'vigilante' ) . $this->locked_file_settings_message() );
7597 return;
7598 }
7599
7600 $presets = $this->settings->get_presets();
7601
7602 if ( ! isset( $presets[ $preset ] ) ) {
7603 wp_send_json_error( __( 'Invalid preset.', 'vigilante' ) );
7604 }
7605
7606 $preset_options = $presets[ $preset ];
7607 unset( $preset_options['name'], $preset_options['description'] );
7608
7609 // Layer the preset on top of the user's CURRENT configuration, not on
7610 // top of defaults. This way applying a preset only changes the fields
7611 // the preset explicitly mentions; everything else stays as the user
7612 // had it. For example, applying Maximum will not flip HSTS off if the
7613 // user had it on — Maximum doesn't touch HSTS, so it's left alone.
7614 // Use "Reset to Defaults" if a clean slate is needed.
7615 $current = get_option( Vigilante_Settings::OPTION_NAME, array() );
7616 if ( ! is_array( $current ) ) {
7617 $current = array();
7618 }
7619 // Make sure all known keys exist before merging — the merge does not
7620 // invent keys that are missing on both sides.
7621 $current = Vigilante_Settings::merge_preset( $this->settings->get_default_options(), $current );
7622
7623 $merged = Vigilante_Settings::merge_preset( $current, $preset_options );
7624 $merged = Vigilante_Settings::keep_locked_file_settings( $merged, get_option( Vigilante_Settings::OPTION_NAME, array() ) );
7625
7626 update_option( Vigilante_Settings::OPTION_NAME, $merged );
7627 $this->settings->clear_cache();
7628
7629 // Save active preset
7630 update_option( 'vigilante_active_preset', $preset );
7631
7632 // Apply file changes after preset
7633 $this->apply_all_file_changes( $merged );
7634
7635 wp_send_json_success( __( 'Preset applied successfully.', 'vigilante' ) . $this->locked_file_settings_message() );
7636 }
7637
7638 /**
7639 * AJAX: Reset a specific section to defaults
7640 */
7641 public function ajax_reset_section() {
7642 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7643
7644 if ( ! current_user_can( 'manage_options' ) ) {
7645 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7646 }
7647
7648 $section = isset( $_POST['section'] ) ? sanitize_key( $_POST['section'] ) : '';
7649
7650 if ( empty( $section ) ) {
7651 wp_send_json_error( __( 'No section specified.', 'vigilante' ) );
7652 }
7653
7654 // Get current options and defaults. get_defaults_preserving_user_data()
7655 // applies the tweaks a fresh installation gets, so the button and a new
7656 // install agree, and keeps whatever the owner typed in.
7657 $current_options = $this->settings->get_all_options();
7658 $defaults = Vigilante_Settings::get_defaults_preserving_user_data( $current_options );
7659
7660 // Check if section exists in defaults
7661 if ( ! isset( $defaults[ $section ] ) ) {
7662 wp_send_json_error( __( 'Invalid section.', 'vigilante' ) );
7663 }
7664
7665 $new_values = $defaults[ $section ];
7666
7667 /*
7668 * On a subsite, the settings written to wp-config.php and .htaccess are
7669 * the main site's business. Resetting the local copy of those would only
7670 * make this screen disagree with the file, so they are carried over
7671 * untouched, and a section that is nothing but shared settings is not
7672 * reset at all. On the main site, a user without network rights keeps
7673 * the ones the shared files are built from as well (2.11.6).
7674 */
7675 $locked = Vigilante_Settings::get_locked_file_settings();
7676
7677 if ( isset( $locked[ $section ] ) && true === $locked[ $section ] ) {
7678 wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
7679 }
7680
7681 $current_options[ $section ] = $new_values;
7682 $current_options = Vigilante_Settings::keep_locked_file_settings( $current_options, get_option( Vigilante_Settings::OPTION_NAME, array() ) );
7683
7684 // Save
7685 update_option( Vigilante_Settings::OPTION_NAME, $current_options );
7686 $this->settings->clear_cache();
7687
7688 // Apply file changes if needed
7689 $this->apply_section_changes( $section, $current_options );
7690
7691 wp_send_json_success( array(
7692 'message' => __( 'Section reset to defaults.', 'vigilante' ),
7693 'section' => $section,
7694 'reload' => true,
7695 ) );
7696 }
7697
7698 /**
7699 * AJAX: Clear lockouts
7700 */
7701 public function ajax_clear_lockouts() {
7702 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7703
7704 if ( ! current_user_can( 'manage_options' ) ) {
7705 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7706 }
7707
7708 $ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : '';
7709
7710 if ( ! empty( $ip ) ) {
7711 $this->database->clear_lockout( $ip );
7712 } else {
7713 $this->database->clear_all_lockouts();
7714 }
7715
7716 wp_send_json_success( __( 'Lockouts cleared.', 'vigilante' ) );
7717 }
7718
7719 /**
7720 * AJAX: Clear logs
7721 */
7722 public function ajax_clear_logs() {
7723 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7724
7725 if ( ! current_user_can( 'manage_options' ) ) {
7726 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7727 }
7728
7729 if ( $this->activity_log ) {
7730 $result = $this->activity_log->clear_all_logs();
7731 if ( $result ) {
7732 wp_send_json_success( __( 'Logs cleared.', 'vigilante' ) );
7733 } else {
7734 wp_send_json_error( __( 'Failed to clear logs.', 'vigilante' ) );
7735 }
7736 } else {
7737 wp_send_json_error( __( 'Activity log not available.', 'vigilante' ) );
7738 }
7739 }
7740
7741 /**
7742 * AJAX: Run file integrity scan.
7743 *
7744 * Triggers the file integrity scan, which now also runs the closed plugins
7745 * check at the end when the `check_closed_plugins` toggle is on. Activity
7746 * log is passed through so Security Audit entries (both file-level and
7747 * plugin-status) are recorded from this entry point.
7748 */
7749 public function ajax_run_scan() {
7750 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7751
7752 if ( ! current_user_can( 'manage_options' ) ) {
7753 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7754 }
7755
7756 // Clear previous results before running new scan
7757 delete_option( 'vigilante_last_integrity_results' );
7758 delete_option( 'vigilante_last_integrity_scan' );
7759
7760 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log );
7761 $results = $file_integrity->run_scan();
7762
7763 // Save new results
7764 update_option( 'vigilante_last_integrity_scan', time() );
7765 update_option( 'vigilante_last_integrity_results', $results );
7766
7767 // On the main site the scan does compute the lines of wp-config.php and
7768 // .htaccess, for the network administrator. Somebody without network
7769 // rights gets the change and its sizes, not the lines.
7770 if ( $this->critical_approval_locked() && ! empty( $results['modified'] ) && is_array( $results['modified'] ) ) {
7771 foreach ( $results['modified'] as $index => $item ) {
7772 if ( is_array( $item ) && 'critical_config' === ( $item['type'] ?? '' ) ) {
7773 $results['modified'][ $index ]['diff'] = Vigilante_File_Integrity::network_only_diff();
7774 }
7775 }
7776 }
7777
7778 wp_send_json_success( array(
7779 'message' => __( 'Scan completed.', 'vigilante' ),
7780 'results' => $results,
7781 'ignored_count' => count( get_option( 'vigilante_ignored_files', array() ) ),
7782 ) );
7783 }
7784
7785 /**
7786 * AJAX: Clear scan results.
7787 *
7788 * Wipes visually everything inside "Last Scan Results": file scan findings
7789 * (Suspicious / Modified / Extra / Critical Config), file hashes and the
7790 * Closed + Removed Plugins block.
7791 *
7792 * Implementation detail to keep persistence intact:
7793 * - We delete the file scan options + hashes outright.
7794 * - For plugin status we ONLY delete the last_check timestamp — the state
7795 * map and the ignore list are preserved in DB. The render gates the
7796 * plugin_status subsections on last_check > 0, so they hide after
7797 * Clear (visual reset) and reappear on the next Run Scan Now with the
7798 * state intact. This avoids degrading a 'removed' slug (404 without
7799 * metadata) back to 'not_in_repo' on the next scan, which would
7800 * silently lose the alert.
7801 *
7802 * The Ignored Files list and the Ignored Closed + Removed Plugins list
7803 * are preserved on purpose; each has its own explicit "Clear All …"
7804 * button.
7805 */
7806 public function ajax_clear_scan() {
7807 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7808
7809 if ( ! current_user_can( 'manage_options' ) ) {
7810 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7811 }
7812
7813 $results = get_option( 'vigilante_last_integrity_results' );
7814 $scanned_at = get_option( 'vigilante_last_integrity_scan' );
7815
7816 delete_option( 'vigilante_last_integrity_results' );
7817 delete_option( 'vigilante_last_integrity_scan' );
7818
7819 /*
7820 * A pending change to wp-config.php or the root .htaccess is closed by
7821 * approving it, which takes the network. Clearing the results was one
7822 * more way to close it without, until the next scan: the ignore list was
7823 * shut in 2.11.8 and this button was left open, found by the cross
7824 * review of 2.11.8. So for somebody who cannot approve, those entries
7825 * stay and everything else goes.
7826 */
7827 if ( $this->critical_approval_locked() && is_array( $results ) && ! empty( $results['modified'] ) && is_array( $results['modified'] ) ) {
7828 $critical = array_values(
7829 array_filter(
7830 $results['modified'],
7831 function ( $item ) {
7832 return is_array( $item ) && 'critical_config' === ( $item['type'] ?? '' );
7833 }
7834 )
7835 );
7836
7837 if ( $critical ) {
7838 $results['modified'] = $critical;
7839 $results['suspicious'] = array();
7840 $results['extra'] = array();
7841 update_option( 'vigilante_last_integrity_results', $results );
7842 update_option( 'vigilante_last_integrity_scan', $scanned_at ? $scanned_at : time() );
7843 }
7844 }
7845
7846 if ( $this->database ) {
7847 $this->database->clear_file_hashes();
7848 }
7849
7850 // Visual reset of plugin_status block without touching the state map
7851 // or the ignored list. See PHPDoc above for the rationale.
7852 delete_option( 'vigilante_plugin_status_last_check' );
7853
7854 wp_send_json_success( __( 'Scan results cleared.', 'vigilante' ) );
7855 }
7856
7857 /**
7858 * AJAX: Ignore a file from scan results
7859 */
7860 public function ajax_ignore_file() {
7861 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7862
7863 if ( ! current_user_can( 'manage_options' ) ) {
7864 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7865 }
7866
7867 $file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : '';
7868
7869 if ( empty( $file ) ) {
7870 wp_send_json_error( __( 'No file specified.', 'vigilante' ) );
7871 }
7872
7873 // A change to a shared file is closed by approving it, and approving it
7874 // takes the network. Ignoring it would close the same warning without.
7875 if ( $this->critical_approval_locked() && in_array( $file, array( 'wp-config.php', '.htaccess' ), true ) ) {
7876 wp_send_json_error( $this->critical_approval_notice() );
7877 }
7878
7879 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
7880 $file_integrity->ignore_file( $file );
7881
7882 // Also remove the file from stored scan results so UI updates
7883 $results = get_option( 'vigilante_last_integrity_results' );
7884 if ( $results ) {
7885 foreach ( array( 'modified', 'suspicious', 'extra' ) as $category ) {
7886 if ( ! empty( $results[ $category ] ) ) {
7887 $results[ $category ] = array_values(
7888 array_filter(
7889 $results[ $category ],
7890 function ( $item ) use ( $file ) {
7891 return ( is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item ) !== $file;
7892 }
7893 )
7894 );
7895 }
7896 }
7897 update_option( 'vigilante_last_integrity_results', $results );
7898 }
7899
7900 wp_send_json_success( __( 'File added to ignored list.', 'vigilante' ) );
7901 }
7902
7903 /**
7904 * AJAX: Stop ignoring a file
7905 */
7906 public function ajax_unignore_file() {
7907 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7908
7909 if ( ! current_user_can( 'manage_options' ) ) {
7910 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7911 }
7912
7913 $file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : '';
7914
7915 if ( empty( $file ) ) {
7916 wp_send_json_error( __( 'No file specified.', 'vigilante' ) );
7917 }
7918
7919 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
7920 $file_integrity->unignore_file( $file );
7921
7922 wp_send_json_success( __( 'File removed from ignored list.', 'vigilante' ) );
7923 }
7924
7925 /**
7926 * AJAX: Bulk ignore multiple files at once
7927 *
7928 * Processes a single batch into ignored list and prunes them from the
7929 * stored scan results so the UI updates without a re-scan.
7930 */
7931 public function ajax_bulk_ignore_files() {
7932 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
7933
7934 if ( ! current_user_can( 'manage_options' ) ) {
7935 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7936 }
7937
7938 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below per-item.
7939 $raw_files = isset( $_POST['files'] ) ? wp_unslash( $_POST['files'] ) : array();
7940 if ( ! is_array( $raw_files ) ) {
7941 wp_send_json_error( __( 'Invalid request.', 'vigilante' ) );
7942 }
7943
7944 $files = array();
7945 $shared = $this->critical_approval_locked() ? array( 'wp-config.php', '.htaccess' ) : array();
7946 foreach ( $raw_files as $f ) {
7947 $clean = sanitize_text_field( $f );
7948 // Same rule as ajax_ignore_file() for the two shared files.
7949 if ( '' !== $clean && ! in_array( $clean, $shared, true ) ) {
7950 $files[] = $clean;
7951 }
7952 }
7953
7954 if ( empty( $files ) ) {
7955 wp_send_json_error( __( 'No files selected.', 'vigilante' ) );
7956 }
7957
7958 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
7959 $count = 0;
7960 foreach ( $files as $file ) {
7961 $file_integrity->ignore_file( $file );
7962 $count++;
7963 }
7964
7965 // Also prune the stored scan results so the UI matches the new ignore list.
7966 $results = get_option( 'vigilante_last_integrity_results' );
7967 if ( $results ) {
7968 $files_set = array_flip( $files );
7969 foreach ( array( 'modified', 'suspicious', 'extra' ) as $category ) {
7970 if ( ! empty( $results[ $category ] ) ) {
7971 $results[ $category ] = array_values(
7972 array_filter(
7973 $results[ $category ],
7974 function ( $item ) use ( $files_set ) {
7975 $path = is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item;
7976 return ! isset( $files_set[ $path ] );
7977 }
7978 )
7979 );
7980 }
7981 }
7982 update_option( 'vigilante_last_integrity_results', $results );
7983 }
7984
7985 wp_send_json_success(
7986 array(
7987 'count' => $count,
7988 'message' => sprintf(
7989 /* translators: %d: number of files added to the ignored list */
7990 _n( '%d file added to ignored list.', '%d files added to ignored list.', $count, 'vigilante' ),
7991 $count
7992 ),
7993 )
7994 );
7995 }
7996
7997 /**
7998 * AJAX: Bulk un-ignore multiple files at once
7999 */
8000 public function ajax_bulk_unignore_files() {
8001 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
8002
8003 if ( ! current_user_can( 'manage_options' ) ) {
8004 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
8005 }
8006
8007 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below per-item.
8008 $raw_files = isset( $_POST['files'] ) ? wp_unslash( $_POST['files'] ) : array();
8009 if ( ! is_array( $raw_files ) ) {
8010 wp_send_json_error( __( 'Invalid request.', 'vigilante' ) );
8011 }
8012
8013 $files = array();
8014 foreach ( $raw_files as $f ) {
8015 $clean = sanitize_text_field( $f );
8016 if ( '' !== $clean ) {
8017 $files[] = $clean;
8018 }
8019 }
8020
8021 if ( empty( $files ) ) {
8022 wp_send_json_error( __( 'No files selected.', 'vigilante' ) );
8023 }
8024
8025 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
8026 $count = 0;
8027 foreach ( $files as $file ) {
8028 $file_integrity->unignore_file( $file );
8029 $count++;
8030 }
8031
8032 wp_send_json_success(
8033 array(
8034 'count' => $count,
8035 'message' => sprintf(
8036 /* translators: %d: number of files removed from the ignored list */
8037 _n( '%d file removed from ignored list.', '%d files removed from ignored list.', $count, 'vigilante' ),
8038 $count
8039 ),
8040 )
8041 );
8042 }
8043
8044 /**
8045 * AJAX: Clear all ignored files
8046 */
8047 public function ajax_clear_ignored() {
8048 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
8049
8050 if ( ! current_user_can( 'manage_options' ) ) {
8051 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
8052 }
8053
8054 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
8055 $file_integrity->clear_ignored_files();
8056
8057 wp_send_json_success( __( 'Ignored files list cleared.', 'vigilante' ) );
8058 }
8059
8060 /**
8061 * AJAX: Ignore a closed/removed plugin slug so it stops appearing in the
8062 * main list and email digests. The plugin keeps running on the site;
8063 * silencing is purely cosmetic and reversible.
8064 */
8065 public function ajax_ignore_closed_plugin() {
8066 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
8067
8068 if ( ! current_user_can( 'manage_options' ) ) {
8069 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
8070 }
8071
8072 $slug = isset( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
8073 if ( '' === $slug ) {
8074 wp_send_json_error( __( 'No slug specified.', 'vigilante' ) );
8075 }
8076
8077 if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) {
8078 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
8079 }
8080 $checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
8081 $checker->ignore_slug( $slug );
8082
8083 wp_send_json_success( __( 'Plugin added to the ignored list.', 'vigilante' ) );
8084 }
8085
8086 /**
8087 * AJAX: Stop ignoring a previously-ignored closed/removed plugin slug.
8088 */
8089 public function ajax_unignore_closed_plugin() {
8090 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
8091
8092 if ( ! current_user_can( 'manage_options' ) ) {
8093 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
8094 }
8095
8096 $slug = isset( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
8097 if ( '' === $slug ) {
8098 wp_send_json_error( __( 'No slug specified.', 'vigilante' ) );
8099 }
8100
8101 if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) {
8102 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
8103 }
8104 $checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
8105 $checker->unignore_slug( $slug );
8106
8107 wp_send_json_success( __( 'Plugin removed from the ignored list.', 'vigilante' ) );
8108 }
8109
8110 /**
8111 * AJAX: Clear the entire ignored-closed-plugins list.
8112 */
8113 public function ajax_clear_ignored_closed_plugins() {
8114 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
8115
8116 if ( ! current_user_can( 'manage_options' ) ) {
8117 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
8118 }
8119
8120 if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) {
8121 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
8122 }
8123 $checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
8124 $checker->clear_ignored();
8125
8126 wp_send_json_success( __( 'Ignored closed plugins list cleared.', 'vigilante' ) );
8127 }
8128
8129 /**
8130 * AJAX: Test security headers
8131 */
8132 public function ajax_test_headers() {
8133 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
8134
8135 if ( ! current_user_can( 'manage_options' ) ) {
8136 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
8137 }
8138
8139 // Get security grade from settings (not from actual HTTP request)
8140 $security_headers = new Vigilante_Security_Headers( $this->settings );
8141 $results = $security_headers->get_security_grade();
8142
8143 wp_send_json_success( $results );
8144 }
8145
8146 /**
8147 * Sanitize section data (kept for compatibility)
8148 *
8149 * @param string $section Section name.
8150 * @param array $data Section data.
8151 * @return array
8152 */
8153 private function sanitize_section_data( $section, $data ) {
8154 $sanitized = array();
8155
8156 foreach ( $data as $key => $value ) {
8157 $key = sanitize_key( $key );
8158
8159 if ( is_array( $value ) ) {
8160 $sanitized[ $key ] = $this->sanitize_section_data( $key, $value );
8161 } elseif ( is_numeric( $value ) ) {
8162 $sanitized[ $key ] = intval( $value );
8163 } else {
8164 $sanitized[ $key ] = sanitize_text_field( $value );
8165 }
8166 }
8167
8168 return $sanitized;
8169 }
8170
8171 /**
8172 * Apply changes after saving settings
8173 *
8174 * @param string $section Section that was updated.
8175 * @param array $all_options All options.
8176 */
8177 private function apply_section_changes( $section, $all_options ) {
8178 // Create fresh settings instance to ensure we have the latest data
8179 $fresh_settings = new Vigilante_Settings();
8180
8181 // The shared Vigilant .htaccess block carries BOTH the firewall's
8182 // file-protection rules and the server-signature / fingerprinting rules
8183 // that are configured under Security Headers. So it must be regenerated
8184 // whenever either section (or the module toggles) changes, not only on
8185 // the firewall save — otherwise toggling "Hide server signature" or
8186 // "Remove fingerprinting headers" never reaches the .htaccess.
8187 if ( in_array( $section, array( 'firewall', 'security_headers', 'modules' ), true ) ) {
8188 $htaccess = new Vigilante_Htaccess_Protection( $fresh_settings );
8189 $sh = $fresh_settings->get_section( 'security_headers' );
8190 $needs_htaccess_block = ! empty( $all_options['modules']['firewall'] )
8191 || ! empty( $sh['hide_server_signature'] )
8192 || ! empty( $sh['remove_fingerprinting_headers'] );
8193
8194 if ( $needs_htaccess_block ) {
8195 $htaccess->apply_rules();
8196 } else {
8197 $htaccess->remove_rules();
8198 }
8199 }
8200
8201 // Regenerate the HTTP security headers (sent by PHP) for the headers section.
8202 if ( 'security_headers' === $section || 'modules' === $section ) {
8203 $security_headers = new Vigilante_Security_Headers( $fresh_settings );
8204 $headers_enabled = ! empty( $all_options['modules']['security_headers'] );
8205
8206 if ( $headers_enabled ) {
8207 $security_headers->apply_rules();
8208 } else {
8209 $security_headers->remove_rules();
8210 }
8211 }
8212
8213 // Regenerate wp-config for wp_hardening section
8214 if ( 'wp_hardening' === $section || 'modules' === $section ) {
8215 $wpconfig = new Vigilante_Wpconfig_Security( $fresh_settings );
8216 $hardening_enabled = ! empty( $all_options['modules']['wp_hardening'] );
8217
8218 if ( $hardening_enabled ) {
8219 $wpconfig->apply_security_constants();
8220 } else {
8221 $wpconfig->remove_constants();
8222 }
8223
8224 // Apply WordPress options for comments/pingbacks
8225 $hardening_options = $all_options['wp_hardening'] ?? array();
8226
8227 if ( $hardening_enabled ) {
8228 // Pingbacks
8229 if ( ! empty( $hardening_options['disable_pingbacks'] ) ) {
8230 update_option( 'default_pingback_flag', 0 );
8231 } else {
8232 // Restore default: pingbacks enabled
8233 update_option( 'default_pingback_flag', 1 );
8234 }
8235
8236 // Trackbacks and ping status
8237 // Only close if either pingbacks OR trackbacks are disabled
8238 if ( ! empty( $hardening_options['disable_pingbacks'] ) || ! empty( $hardening_options['disable_trackbacks'] ) ) {
8239 update_option( 'default_ping_status', 'closed' );
8240 } else {
8241 // Restore default: pings open
8242 update_option( 'default_ping_status', 'open' );
8243 }
8244
8245 // Comment moderation
8246 if ( ! empty( $hardening_options['require_comment_moderation'] ) ) {
8247 update_option( 'comment_moderation', 1 );
8248 } else {
8249 // Restore default: no moderation required
8250 update_option( 'comment_moderation', 0 );
8251 }
8252 }
8253 }
8254
8255 // Trim activity log entries immediately when limits change
8256 if ( 'activity_log' === $section && $this->activity_log ) {
8257 $this->activity_log->cleanup_old_logs();
8258 }
8259
8260 // Flush rewrite rules if login URL changed
8261 if ( 'login_security' === $section ) {
8262 $login_options = $all_options['login_security'] ?? array();
8263 if ( ! empty( $login_options['custom_login_url'] ) ) {
8264 delete_option( 'vigilante_login_rules_version' );
8265 }
8266 }
8267
8268 // Log the settings change with readable section name
8269 if ( $this->activity_log ) {
8270 $section_names = array(
8271 'firewall' => __( 'Firewall', 'vigilante' ),
8272 'login_security' => __( 'Login Security', 'vigilante' ),
8273 'security_headers' => __( 'Security Headers', 'vigilante' ),
8274 'rest_api_security'=> __( 'REST API Security', 'vigilante' ),
8275 'user_security' => __( 'User Security', 'vigilante' ),
8276 'wp_hardening' => __( 'WP Hardening', 'vigilante' ),
8277 'activity_log' => __( 'Security Audit', 'vigilante' ),
8278 'file_integrity' => __( 'File Integrity', 'vigilante' ),
8279 'email' => __( 'Notification Settings', 'vigilante' ),
8280 'backup' => __( 'Backup', 'vigilante' ),
8281 'advanced' => __( 'Advanced', 'vigilante' ),
8282 'modules' => __( 'Modules', 'vigilante' ),
8283 );
8284 $display_name = isset( $section_names[ $section ] ) ? $section_names[ $section ] : $section;
8285
8286 $this->activity_log->log(
8287 'settings',
8288 'settings_updated',
8289 sprintf(
8290 /* translators: %s: Section name */
8291 __( 'Settings updated: %s', 'vigilante' ),
8292 $display_name
8293 ),
8294 array( 'section' => $section ),
8295 'info'
8296 );
8297 }
8298 }
8299
8300 /**
8301 * Apply all file changes (htaccess, wp-config) based on current options
8302 *
8303 * Used after preset, import, or reset operations
8304 *
8305 * @param array $all_options All plugin options.
8306 */
8307 private function apply_all_file_changes( $all_options ) {
8308 // Refresh settings cache first
8309 $this->settings->clear_cache();
8310
8311 // Create fresh settings instance
8312 $fresh_settings = new Vigilante_Settings();
8313
8314 // Apply firewall htaccess changes
8315 $htaccess = new Vigilante_Htaccess_Protection( $fresh_settings );
8316 $firewall_enabled = ! empty( $all_options['modules']['firewall'] );
8317
8318 if ( $firewall_enabled ) {
8319 $htaccess->apply_rules();
8320 } else {
8321 $htaccess->remove_rules();
8322 }
8323
8324 // Apply security headers htaccess changes
8325 $security_headers = new Vigilante_Security_Headers( $fresh_settings );
8326 $headers_enabled = ! empty( $all_options['modules']['security_headers'] );
8327
8328 if ( $headers_enabled ) {
8329 $security_headers->apply_rules();
8330 } else {
8331 $security_headers->remove_rules();
8332 }
8333
8334 // Apply wp-config changes
8335 $wpconfig = new Vigilante_Wpconfig_Security( $fresh_settings );
8336 $hardening_enabled = ! empty( $all_options['modules']['wp_hardening'] );
8337
8338 if ( $hardening_enabled ) {
8339 $wpconfig->apply_security_constants();
8340 } else {
8341 $wpconfig->remove_constants();
8342 }
8343
8344 // Apply WordPress options for comments/pingbacks
8345 $hardening_options = $all_options['wp_hardening'] ?? array();
8346
8347 if ( $hardening_enabled ) {
8348 // Pingbacks
8349 if ( ! empty( $hardening_options['disable_pingbacks'] ) ) {
8350 update_option( 'default_pingback_flag', 0 );
8351 } else {
8352 // Restore default: pingbacks enabled
8353 update_option( 'default_pingback_flag', 1 );
8354 }
8355
8356 // Trackbacks and ping status
8357 // Only close if either pingbacks OR trackbacks are disabled
8358 if ( ! empty( $hardening_options['disable_pingbacks'] ) || ! empty( $hardening_options['disable_trackbacks'] ) ) {
8359 update_option( 'default_ping_status', 'closed' );
8360 } else {
8361 // Restore default: pings open
8362 update_option( 'default_ping_status', 'open' );
8363 }
8364
8365 // Comment moderation
8366 if ( ! empty( $hardening_options['require_comment_moderation'] ) ) {
8367 update_option( 'comment_moderation', 1 );
8368 } else {
8369 // Restore default: no moderation required
8370 update_option( 'comment_moderation', 0 );
8371 }
8372 }
8373
8374 // Log the change
8375 if ( $this->activity_log ) {
8376 $this->activity_log->log(
8377 'settings',
8378 'bulk_settings_applied',
8379 __( 'Bulk settings applied (preset/import/reset)', 'vigilante' ),
8380 array(),
8381 'info'
8382 );
8383 }
8384 }
8385 }