PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.12
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.12
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
← All changes | admin/class-admin.php +1463 -273 2.9.62.11.12 View file →
@@ -27,8 +27,9 @@
27 27
28 28 use Vigilante_Admin_Ajax;
29 29 use Vigilante_Admin_Analyzer_Ajax;
30 30 use Vigilante_Admin_Audit_Alerts_Ajax;
31 + use Vigilante_Admin_Recovery_Ajax;
31 32
32 33 /**
33 34 * Settings instance
34 35 *
@@ -182,8 +183,13 @@
182 183 add_action( 'wp_ajax_vigilante_analyzer_history', array( $this, 'ajax_analyzer_history' ) );
183 184 add_action( 'wp_ajax_vigilante_analyzer_dismiss_notice', array( $this, 'ajax_analyzer_dismiss_notice' ) );
184 185 add_action( 'wp_ajax_vigilante_analyzer_save_settings', array( $this, 'ajax_analyzer_save_settings' ) );
185 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 +
186 192 // Shared "Send test email" handler — Notification settings, File Integrity, Audit Alerts (v2.8.0)
187 193 add_action( 'wp_ajax_vigilante_send_test_email', array( $this, 'ajax_send_test_email' ) );
188 194
189 195 // Run migrations on admin load
@@ -193,8 +199,26 @@
193 199 /**
194 200 * Run database migrations based on stored version
195 201 */
196 202 public function run_migrations() {
203 + /*
204 + * admin-ajax.php fires admin_init before it decides who is asking
205 + * (wp-admin/admin-ajax.php:45), so until 2.11.10 an anonymous POST to
206 + * admin-ajax.php with any action ran every pending migration. That is
207 + * not a read: the migrations rewrite wp-config.php through
208 + * apply_security_constants(), rewrite the root .htaccess, move user meta
209 + * of the whole network and can rebuild the file integrity baseline,
210 + * taking whatever is on disk as approved. Reproduced on 12 sep 2026 with
211 + * curl and no cookies, and found by the file-by-file review of 2.11.10.
212 + *
213 + * Migrations are maintenance for whoever administers the site, so they
214 + * wait for an administrator to load a screen. Nothing is lost by
215 + * waiting: every migration is idempotent and version gated.
216 + */
217 + if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) {
218 + return;
219 + }
220 +
197 221 $db_version = get_option( 'vigilante_db_version', '0' );
198 222
199 223 // 1.2.3: Fix IP lists corrupted by sanitize_text_field stripping newlines
200 224 if ( version_compare( $db_version, '1.2.3', '<' ) ) {
@@ -267,9 +291,29 @@
267 291 if ( ! class_exists( 'Vigilante_File_Integrity' ) ) {
268 292 require_once VIGILANTE_INCLUDES_DIR . 'class-file-integrity.php';
269 293 }
270 294 $fi = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log );
271 - $fi->regenerate_all_baselines();
295 +
296 + /*
297 + * Only when there is nothing on record. This migration exists to
298 + * create the baseline that did not exist, never to discard the one
299 + * the owner approved: rebuilding it from the files takes whatever
300 + * is on disk right now as approved, so a wp-config.php modified and
301 + * awaiting review would be blessed in silence.
302 + *
303 + * And this is not theory. vigilante_db_version is written on two
304 + * different scales into the same option: this file counts in plugin
305 + * versions (2.11.0) and Vigilante_Database counts in schema
306 + * versions, currently 1.4.0 (class-database.php:322 and :380). For
307 + * version_compare, 1.4.0 is LOWER than 1.14.0, so any site whose
308 + * option was last written by the schema runs this migration again.
309 + * Measured on the Multisite install on 10 sep 2026: one of the three
310 + * sites was sitting on 1.4.0.
311 + */
312 + if ( ! $fi->get_critical_files_baseline() ) {
313 + $fi->regenerate_all_baselines();
314 + }
315 +
272 316 update_option( 'vigilante_db_version', '1.14.0' );
273 317 }
274 318
275 319 // 2.0.0: Move hide_server_signature and remove_fingerprinting_headers
@@ -343,11 +387,248 @@
343 387 }
344 388
345 389 update_option( 'vigilante_db_version', '2.9.3' );
346 390 }
391 +
392 + /*
393 + * 2.9.8: the mixed content handling changes shape. "Upgrade Insecure
394 + * Requests" becomes a setting of its own, and Fix Mixed Content ships
395 + * off, where before it shipped on and carried the directive with it.
396 + * Both have to be written down for sites that are updating, so their
397 + * pages keep loading exactly what they loaded yesterday.
398 + *
399 + * Read the RAW stored options, not get_section(): that one merges the
400 + * defaults, so a site that never stored the key would be read with the
401 + * new default and silently lose the behaviour it had. Absent means the
402 + * site was running on the old default, which was on.
403 + */
404 + if ( version_compare( $db_version, '2.9.8', '<' ) ) {
405 + $raw = get_option( Vigilante_Settings::OPTION_NAME, array() );
406 + $stored = ( is_array( $raw ) && isset( $raw['security_headers'] ) && is_array( $raw['security_headers'] ) ) ? $raw['security_headers'] : array();
407 + $had_fix = array_key_exists( 'fix_mixed_content', $stored ) ? ! empty( $stored['fix_mixed_content'] ) : true;
408 +
409 + /*
410 + * Merge, never replace. update_section() overwrites the whole
411 + * section, so passing just these two keys wiped every other header
412 + * setting the site had stored (HSTS, CSP, cross-origin policies,
413 + * the HTTPS switches, Server Identity) and left the screen showing
414 + * factory defaults while the .htaccess kept serving the old values.
415 + */
416 + $this->settings->update_section(
417 + 'security_headers',
418 + array_merge(
419 + $stored,
420 + array(
421 + 'fix_mixed_content' => $had_fix,
422 + 'upgrade_insecure_requests' => $had_fix,
423 + )
424 + )
425 + );
426 +
427 + update_option( 'vigilante_db_version', '2.9.8' );
428 + }
429 +
430 + /*
431 + * 2.9.9: drop the settings that no code has read for versions.
432 + *
433 + * They were carried in the defaults and therefore written into every
434 + * saved configuration, they show up in an exported configuration, and
435 + * anyone reading them assumes a feature exists behind them. Removing
436 + * them from the defaults is not enough: the stored copies survive, so
437 + * they are swept here too. Nothing reads them, so nothing changes.
438 + */
439 + if ( version_compare( $db_version, '2.9.9', '<' ) ) {
440 + $raw = get_option( Vigilante_Settings::OPTION_NAME, array() );
441 + $dead = array(
442 + 'firewall' => array( 'country_blocking', 'protected_file_extensions' ),
443 + 'file_integrity' => array( 'suspicious_patterns' ),
444 + 'backup' => array( 'auto_backup', 'backup_before_update' ),
445 + 'advanced' => array( 'block_author_archives', 'disable_embeds', 'uninstall_cleanup', 'debug_mode' ),
446 + );
447 +
448 + $changed = false;
449 + foreach ( $dead as $section => $keys ) {
450 + if ( ! isset( $raw[ $section ] ) || ! is_array( $raw[ $section ] ) ) {
451 + continue;
452 + }
453 + foreach ( $keys as $key ) {
454 + if ( array_key_exists( $key, $raw[ $section ] ) ) {
455 + unset( $raw[ $section ][ $key ] );
456 + $changed = true;
457 + }
458 + }
459 + }
460 +
461 + if ( $changed ) {
462 + update_option( Vigilante_Settings::OPTION_NAME, $raw );
463 + }
464 +
465 + update_option( 'vigilante_db_version', '2.9.9' );
466 + }
467 +
468 + /*
469 + * 2.11.0: security release (audit of 28 Aug 2026). Runs here and not
470 + * from Vigilante_Database::needs_update(): this option is shared with
471 + * that class, and on any updated site it already holds a plugin version
472 + * (2.9.9 or later), so a bump of DB_VERSION would never fire.
473 + * create_tables() widens the email code column through dbDelta (varchar
474 + * 6 to 64, the code is stored hashed since 2.11.0) and purge_for_2_11_0()
475 + * does what dbDelta cannot: it empties the trusted devices, which were
476 + * identified by User-Agent until now (S1), and the pending email codes,
477 + * stored in clear until now (S11). Every remembered device asks for the
478 + * second factor once more after this update, and the changelog says so.
479 + */
480 + if ( version_compare( $db_version, '2.11.0', '<' ) ) {
481 + $this->database->create_tables();
482 + $this->database->purge_for_2_11_0();
483 +
484 + update_option( 'vigilante_db_version', '2.11.0' );
485 + }
486 +
487 + /*
488 + * 2.11.9: clear the raw .htaccess copies that older versions left in
489 + * options, on the first admin load after the update. Uninstall already
490 + * removes them, but that only fires when the plugin is deleted, so a
491 + * site that keeps the plugin carried them until now. Three stores, each
492 + * a copy of a file that can hold secrets (a SetEnv token, an
493 + * Authorization header): the same exposure the wp.org review flagged as
494 + * 4.4, on the paths its fix did not reach.
495 + *
496 + * - vigilante_htaccess_history: up to five raw copies, by design, until
497 + * 2.11.8. The writer is gone, nothing reads it, so it is deleted.
498 + * - vigilante_htaccess_backup: the single rollback buffer, normally
499 + * cleared in the finally of each write; a copy only lingers if a write
500 + * crashed mid-operation. Nothing outside one write reads it, so a
501 + * leftover is deleted.
502 + * - vigilante_htaccess_pre_migration: still read by the header recovery,
503 + * but older versions stored the whole file where only our own block is
504 + * ever used. Truncated to that block, so the feature keeps working and
505 + * nothing outside our markers stays in the option.
506 + */
507 + if ( version_compare( $db_version, '2.11.9', '<' ) ) {
508 + delete_option( 'vigilante_htaccess_history' );
509 + delete_option( 'vigilante_htaccess_backup' );
510 +
511 + $snapshot = get_option( 'vigilante_htaccess_pre_migration' );
512 + if ( is_array( $snapshot ) && isset( $snapshot['content'] ) && '' !== (string) $snapshot['content'] ) {
513 + require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-recovery.php';
514 + $block = Vigilante_Htaccess_Recovery::get_raw_block();
515 +
516 + if ( '' === $block ) {
517 + delete_option( 'vigilante_htaccess_pre_migration' );
518 + } elseif ( $block !== $snapshot['content'] ) {
519 + $snapshot['content'] = $block;
520 + update_option( 'vigilante_htaccess_pre_migration', $snapshot, false );
521 + }
522 + }
523 +
524 + update_option( 'vigilante_db_version', '2.11.9' );
525 + }
526 +
527 + /*
528 + * 2.11.10: the pending-approval flag becomes one per site on a network.
529 + * Until 2.11.9 it was a single global user meta, so the queue was shared
530 + * across the whole network. Moving the key is not enough: the accounts
531 + * already waiting carry the old key, and reading only the new one would
532 + * let them log in. So they are moved here, each to the site it belongs
533 + * to, and the old key is removed only once the new one is written.
534 + */
535 + if ( version_compare( $db_version, '2.11.10', '<' ) ) {
536 + $this->migrate_pending_approval_per_site();
537 +
538 + update_option( 'vigilante_db_version', '2.11.10' );
539 + }
347 540 }
348 541
349 542 /**
543 + * Move the pending-approval flag of a network to a key per site
544 + *
545 + * Runs once for the whole network, not once per site: the data it moves is
546 + * global, so the guard is a network option and any site may be the one that
547 + * does it. On a single site the key does not change and there is nothing to
548 + * do.
549 + *
550 + * Each waiting account goes to its primary site, or to the only site it
551 + * belongs to; one that belongs to none goes to the main site rather than
552 + * nowhere, because losing the flag would silently approve it.
553 + *
554 + * @since 2.11.10
555 + */
556 + private function migrate_pending_approval_per_site() {
557 + global $wpdb;
558 +
559 + if ( ! is_multisite() ) {
560 + return;
561 + }
562 +
563 + if ( get_site_option( 'vigilante_pending_per_site_done' ) ) {
564 + return;
565 + }
566 +
567 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- One-off migration of the plugin's own user meta; the meta API has no "list every user with this key".
568 + $user_ids = $wpdb->get_col(
569 + $wpdb->prepare( "SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s", 'vigilante_pending_approval' )
570 + );
571 +
572 + foreach ( (array) $user_ids as $user_id ) {
573 + $user_id = (int) $user_id;
574 + if ( ! $user_id ) {
575 + continue;
576 + }
577 +
578 + $pending = get_user_meta( $user_id, 'vigilante_pending_approval', true );
579 + $since = get_user_meta( $user_id, 'vigilante_pending_since', true );
580 +
581 + /*
582 + * Every site the account belongs to, not its primary one. The global
583 + * flag does not say where the registration happened, and the first
584 + * version of this guessed the primary blog: an account that
585 + * registered on B while its primary was A came out pending on A and
586 + * free to log in on B, which is the very site it had never been
587 + * approved on. Found by the cross review of 2.11.10.
588 + *
589 + * Marking every site it belongs to fails closed instead: the account
590 + * stays blocked wherever it can log in, and shows up in the queue of
591 + * each of those sites so somebody can actually act on it. An account
592 + * that belongs to no site goes to the main one rather than nowhere,
593 + * because losing the flag would silently approve it.
594 + */
595 + /*
596 + * With $all true, because the default leaves out archived, spam and
597 + * deleted sites (wp-includes/user.php:1113-1117): a site archived on
598 + * the day this runs would lose the flag, and the account would walk
599 + * in unapproved the moment it was brought back. Found by the second
600 + * cross review of 2.11.10.
601 + */
602 + $blog_ids = array();
603 +
604 + foreach ( get_blogs_of_user( $user_id, true ) as $blog ) {
605 + if ( ! empty( $blog->userblog_id ) ) {
606 + $blog_ids[] = (int) $blog->userblog_id;
607 + }
608 + }
609 +
610 + if ( empty( $blog_ids ) ) {
611 + $blog_ids[] = (int) get_main_site_id();
612 + }
613 +
614 + foreach ( array_unique( $blog_ids ) as $blog_id ) {
615 + $prefix = $wpdb->get_blog_prefix( $blog_id );
616 +
617 + update_user_meta( $user_id, $prefix . 'vigilante_pending_approval', $pending );
618 + if ( '' !== $since && false !== $since ) {
619 + update_user_meta( $user_id, $prefix . 'vigilante_pending_since', $since );
620 + }
621 + }
622 +
623 + delete_user_meta( $user_id, 'vigilante_pending_approval' );
624 + delete_user_meta( $user_id, 'vigilante_pending_since' );
625 + }
626 +
627 + update_site_option( 'vigilante_pending_per_site_done', 1 );
628 + }
629 +
630 + /**
350 631 * Migration: Remove orphaned email fields from saved options
351 632 *
352 633 * v1.10.0 centralized notification recipients into email section.
353 634 * Old per-module notify_email fields and dead email section fields
@@ -615,23 +896,32 @@
615 896 if ( ! did_action( 'plugins_loaded' ) ) {
616 897 return 0;
617 898 }
618 899
619 - $registration_approval = $this->settings->get_section( 'user_security' );
620 - $approval_settings = $registration_approval['registration_approval'] ?? array();
621 -
622 - if ( empty( $approval_settings['enabled'] ) ) {
623 - return 0;
624 - }
900 + /*
901 + * Counted whether the feature is on or off. An account already waiting
902 + * stays blocked when it is switched off (see init_enforcement_hooks()),
903 + * so reporting zero there hid people who cannot log in and whom nobody
904 + * could see to approve. Found by the cross review of 2.11.10.
905 + */
625 906
626 907 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Limited results in admin context.
627 - $pending_users = get_users( array(
628 - 'meta_key' => 'vigilante_pending_approval',
908 + $args = array(
909 + 'meta_key' => Vigilante_User_Security::site_user_meta_key( 'vigilante_pending_approval' ),
629 910 'meta_value' => '1',
630 911 'fields' => 'ID',
631 - ) );
912 + );
632 913 // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
633 914
915 + // Same query as Vigilante_User_Security::get_pending_users(), and for the
916 + // same reason: the meta key already scopes this to the site, and adding
917 + // core's membership filter on top hid the accounts that have no role yet.
918 + if ( is_multisite() ) {
919 + $args['blog_id'] = 0;
920 + }
921 +
922 + $pending_users = get_users( $args );
923 +
634 924 return count( $pending_users );
635 925 }
636 926
637 927 /**
@@ -1104,96 +1394,215 @@
1104 1394 );
1105 1395 }
1106 1396
1107 1397 /**
1108 - * Build the settings search index
1398 + * Index behind the settings search box.
1109 1399 *
1110 - * Flat list of searchable entries consumed by the client-side search.
1111 - * Each entry contains tab + section metadata, a translated label and an
1112 - * English fallback so locales with partial translation still match.
1400 + * Each entry points at one settings row. The search matches on the label,
1401 + * on its English original and on 'keywords', which are extra terms someone
1402 + * might type instead of the label itself.
1113 1403 *
1404 + * Those keywords are wrapped in _x() with the context "settings search
1405 + * keywords" so every locale can supply its own: the source strings are in
1406 + * English, and a Spanish user typing "contrasena" or a German one typing
1407 + * "Kennwort" only reaches the password settings if that locale translated
1408 + * them. Translators can add, drop or replace terms freely, one per space;
1409 + * they are never displayed, only matched against what the user types.
1410 + *
1411 + * The list is maintained by hand, so a new settings row needs an entry here
1412 + * or it cannot be found. It had drifted to 68 of 131 rows before 2.9.7.
1413 + *
1114 1414 * @return array
1115 1415 */
1116 1416 private function get_search_index() {
1117 1417 return array(
1118 1418 // Firewall - Main
1119 - array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Block bad bots', 'vigilante' ), 'label_en' => 'Block bad bots', 'keywords' => 'bots robots malos bloquear bad block crawlers arañas scrapers' ),
1120 - array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Block malicious requests', 'vigilante' ), 'label_en' => 'Block malicious requests', 'keywords' => 'malicioso maliciosas peticiones ataques sqli xss rfi lfi ataque exploit injection inyección' ),
1121 - array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Rate limiting', 'vigilante' ), 'label_en' => 'Rate limiting', 'keywords' => 'limitar tasa velocidad throttle peticiones minuto abuso flood' ),
1122 - array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Brute force protection', 'vigilante' ), 'label_en' => 'Brute force protection', 'keywords' => 'fuerza bruta brute force contraseñas ataque login diccionario' ),
1123 - array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'IP Whitelist', 'vigilante' ), 'label_en' => 'IP Whitelist', 'keywords' => 'lista blanca permitida permitidas whitelist allowlist ip direcciones permitir' ),
1124 - array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'IP Blacklist', 'vigilante' ), 'label_en' => 'IP Blacklist', 'keywords' => 'lista negra bloqueada bloqueadas blacklist blocklist denylist ip direcciones bloquear' ),
1125 - array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'User-Agent Whitelist', 'vigilante' ), 'label_en' => 'User-Agent Whitelist', 'keywords' => 'ua user agent agente usuario navegador lista blanca permitida whitelist allowlist' ),
1126 - array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'User-Agent Blacklist', 'vigilante' ), 'label_en' => 'User-Agent Blacklist', 'keywords' => 'ua user agent agente usuario navegador lista negra bloqueada blacklist blocklist denylist' ),
1419 + 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' ) ),
1420 + 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' ) ),
1421 + 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' ) ),
1422 + 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' ) ),
1423 + 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' ) ),
1424 + 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' ) ),
1425 + 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' ) ),
1426 + 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' ) ),
1127 1427 // Firewall - Server Protection
1128 - array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'Directory Browsing', 'vigilante' ), 'label_en' => 'Directory Browsing', 'keywords' => 'directorio navegación listado listing indexing indexado carpetas' ),
1129 - array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'Protect wp-config.php', 'vigilante' ), 'label_en' => 'Protect wp-config.php', 'keywords' => 'proteger wp-config configuración config archivo' ),
1130 - array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'field-protect-wp-cron', 'label' => __( 'Protect wp-cron.php', 'vigilante' ), 'label_en' => 'Protect wp-cron.php', 'keywords' => 'proteger wp-cron cron tareas programadas scheduled tasks bloquear block dos abuse spam htaccess' ),
1131 - array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'Protect wp-includes', 'vigilante' ), 'label_en' => 'Protect wp-includes', 'keywords' => 'proteger wp-includes includes core núcleo archivos' ),
1132 - array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'PHP in Uploads', 'vigilante' ), 'label_en' => 'PHP in Uploads', 'keywords' => 'php uploads subidas archivos bloquear ejecución' ),
1133 - array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'Sensitive Files', 'vigilante' ), 'label_en' => 'Sensitive Files', 'keywords' => 'sensibles sensitive files archivos readme license htaccess log' ),
1134 - array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'Limit HTTP Methods', 'vigilante' ), 'label_en' => 'Limit HTTP Methods', 'keywords' => 'métodos http limit limitar trace options put delete verbs verbos' ),
1428 + 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' ) ),
1429 + 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' ) ),
1430 + 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' ) ),
1431 + 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' ) ),
1432 + 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' ) ),
1433 + 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' ) ),
1434 + 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' ) ),
1135 1435 // Security Headers
1136 - array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'X-Frame-Options', 'vigilante' ), 'label_en' => 'X-Frame-Options', 'keywords' => 'xframe clickjacking iframe cabeceras headers' ),
1137 - array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'X-Content-Type-Options', 'vigilante' ), 'label_en' => 'X-Content-Type-Options', 'keywords' => 'mime sniffing nosniff content type cabeceras headers' ),
1138 - array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Referrer-Policy', 'vigilante' ), 'label_en' => 'Referrer-Policy', 'keywords' => 'referer referrer política privacidad cabeceras headers' ),
1139 - array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'HSTS', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'HSTS', 'vigilante' ), 'label_en' => 'HSTS', 'keywords' => 'strict transport security ssl tls https forzar cabeceras headers' ),
1140 - array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Content Security Policy', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Content Security Policy', 'vigilante' ), 'label_en' => 'Content Security Policy', 'keywords' => 'csp política seguridad contenido xss scripts inline eval cabeceras headers' ),
1141 - array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Server Identity', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Server Signature', 'vigilante' ), 'label_en' => 'Server Signature', 'keywords' => 'firma servidor server signature identidad apache nginx ocultar hide fingerprint protección servidor' ),
1142 - array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Server Identity', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Remove Fingerprinting Headers', 'vigilante' ), 'label_en' => 'Remove Fingerprinting Headers', 'keywords' => 'fingerprint huella identificación cabeceras headers x-powered-by server ocultar eliminar protección servidor' ),
1436 + 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' ) ),
1437 + 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' ) ),
1438 + 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' ) ),
1439 + 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' ) ),
1440 + 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' ) ),
1441 + 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' ) ),
1442 + 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' ) ),
1443 + // Security Headers - Cross-Origin Policies
1444 + 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' ) ),
1445 + 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' ) ),
1446 + 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' ) ),
1143 1447 // Login Security
1144 - array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Custom login URL', 'vigilante' ), 'label_en' => 'Custom login URL', 'keywords' => 'url personalizada login acceso entrar wp-login wp-admin slug ocultar esconder' ),
1145 - array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Two-Factor Authentication', 'vigilante' ), 'label_en' => 'Two-Factor Authentication', 'keywords' => '2fa doble factor autenticación totp google authenticator mfa' ),
1146 - array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( '2FA', 'vigilante' ), 'label_en' => '2FA', 'keywords' => '2fa doble factor autenticación totp google authenticator mfa two factor' ),
1147 - array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Failed login attempts', 'vigilante' ), 'label_en' => 'Failed login attempts', 'keywords' => 'intentos fallidos login acceso failed attempts bloqueo bloqueos contraseña errónea' ),
1148 - array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Lockout', 'vigilante' ), 'label_en' => 'Lockout', 'keywords' => 'bloqueo bloqueado lockout baneo ban duración login' ),
1448 + 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' ) ),
1449 + 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' ) ),
1450 + 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' ) ),
1451 + 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' ) ),
1452 + 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' ) ),
1149 1453 // REST API
1150 - array( 'tab' => 'rest-api', 'tab_label' => __( 'REST API', 'vigilante' ), 'section' => __( 'REST API Security', 'vigilante' ), 'anchor' => 'vigilante-section-rest-api-main', 'label' => __( 'Access Mode', 'vigilante' ), 'label_en' => 'Access Mode', 'keywords' => 'modo acceso rest api público privado autenticado' ),
1151 - array( 'tab' => 'rest-api', 'tab_label' => __( 'REST API', 'vigilante' ), 'section' => __( 'REST API Security', 'vigilante' ), 'anchor' => 'vigilante-section-rest-api-main', 'label' => __( 'Block User Enumeration', 'vigilante' ), 'label_en' => 'Block User Enumeration', 'keywords' => 'enumeración usuarios users block bloquear autores author slug ?author' ),
1152 - array( 'tab' => 'rest-api', 'tab_label' => __( 'REST API', 'vigilante' ), 'section' => __( 'REST API Security', 'vigilante' ), 'anchor' => 'vigilante-section-rest-api-main', 'label' => __( 'Disable JSONP', 'vigilante' ), 'label_en' => 'Disable JSONP', 'keywords' => 'jsonp desactivar deshabilitar disable callback' ),
1454 + 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' ) ),
1455 + 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' ) ),
1456 + 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' ) ),
1153 1457 // User Security
1154 - array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Username protection', 'vigilante' ), 'label_en' => 'Username protection', 'keywords' => 'nombre usuario username admin reservado prohibido protección' ),
1155 - array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Password strength', 'vigilante' ), 'label_en' => 'Password strength', 'keywords' => 'fortaleza fuerza contraseña password débil fuerte complejidad requisitos' ),
1156 - array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Admin monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-users-admin-monitoring', 'label' => __( 'Admin monitoring', 'vigilante' ), 'label_en' => 'Admin monitoring', 'keywords' => 'monitorización administradores admin supervisión alertas cambios' ),
1157 - array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Registration approval', 'vigilante' ), 'anchor' => 'vigilante-section-users-registration', 'label' => __( 'Registration approval', 'vigilante' ), 'label_en' => 'Registration approval', 'keywords' => 'aprobación registro registration moderación nuevos usuarios signup' ),
1158 - array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Session limits', 'vigilante' ), 'anchor' => 'vigilante-section-users-sessions', 'label' => __( 'Session limits', 'vigilante' ), 'label_en' => 'Session limits', 'keywords' => 'sesiones limits límite concurrentes sessions simultáneas' ),
1159 - array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Password expiration', 'vigilante' ), 'label_en' => 'Password expiration', 'keywords' => 'expiración caducidad contraseña password cambiar renovar rotación' ),
1160 - array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Email verification', 'vigilante' ), 'anchor' => 'vigilante-section-users-email-verify', 'label' => __( 'Email verification', 'vigilante' ), 'label_en' => 'Email verification', 'keywords' => 'verificación correo email confirmación validación' ),
1458 + 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' ) ),
1459 + 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' ) ),
1460 + 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' ) ),
1461 + 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' ) ),
1462 + 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' ) ),
1463 + 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' ) ),
1464 + 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' ) ),
1161 1465 // WP Hardening
1162 - array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Database Hardening', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-database', 'label' => __( 'Database Hardening', 'vigilante' ), 'label_en' => 'Database Hardening', 'keywords' => 'base datos database db mysql fortalecer hardening endurecer' ),
1163 - array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Database Hardening', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-database', 'label' => __( 'Database prefix', 'vigilante' ), 'label_en' => 'Database prefix', 'keywords' => 'prefijo base datos database db mysql tabla tablas wp_ cambiar renombrar' ),
1164 - array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-wpconfig', 'label' => __( 'Disable file editing', 'vigilante' ), 'label_en' => 'Disable file editing', 'keywords' => 'desactivar deshabilitar disable edición editor archivos file edit disallow' ),
1165 - array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-wpconfig', 'label' => __( 'Disable plugin/theme installation', 'vigilante' ), 'label_en' => 'Disable plugin/theme installation', 'keywords' => 'desactivar deshabilitar disable instalación plugins temas themes install disallow' ),
1166 - array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-wpconfig', 'label' => __( 'Force SSL admin', 'vigilante' ), 'label_en' => 'Force SSL admin', 'keywords' => 'forzar ssl tls https admin administración certificado' ),
1167 - array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'field-disable-wp-cron', 'label' => __( 'Disable WP Cron', 'vigilante' ), 'label_en' => 'Disable WP Cron', 'keywords' => 'desactivar deshabilitar disable wp cron tareas programadas scheduled real server crontab pseudo' ),
1168 - array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Comment Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-comments', 'label' => __( 'Comment Security', 'vigilante' ), 'label_en' => 'Comment Security', 'keywords' => 'comentarios comments spam protección honeypot autores url' ),
1169 - array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Header Cleanup', 'vigilante' ), 'label_en' => 'Header Cleanup', 'keywords' => 'limpieza cabeceras headers meta tags wordpress generator rsd wlwmanifest' ),
1170 - array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Remove WordPress version', 'vigilante' ), 'label_en' => 'Remove WordPress version', 'keywords' => 'eliminar quitar versión wordpress wp generator meta ocultar hide' ),
1171 - array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'field-remove-wp-version-assets', 'label' => __( 'Remove version from assets', 'vigilante' ), 'label_en' => 'Remove version from assets', 'keywords' => 'eliminar quitar versión wordpress wp ver query string assets recursos urls scripts styles css js cache busting' ),
1172 - array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Disable XML-RPC', 'vigilante' ), 'label_en' => 'Disable XML-RPC', 'keywords' => 'xmlrpc xml-rpc desactivar deshabilitar disable pingback trackback' ),
1173 - array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'RSS Feed Settings', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-rss', 'label' => __( 'RSS Feed Settings', 'vigilante' ), 'label_en' => 'RSS Feed Settings', 'keywords' => 'rss feed sindicación feeds ajustes configuración' ),
1466 + 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' ) ),
1467 + 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' ) ),
1468 + 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' ) ),
1469 + 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' ) ),
1470 + 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' ) ),
1471 + 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' ) ),
1472 + 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' ) ),
1473 + 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' ) ),
1474 + 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' ) ),
1475 + 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' ) ),
1476 + 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' ) ),
1477 + 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' ) ),
1174 1478 // File Integrity
1175 - array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'File Integrity Monitoring', 'vigilante' ), 'label_en' => 'File Integrity Monitoring', 'keywords' => 'integridad archivos monitorización supervisión hash checksum malware cambios' ),
1176 - array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Scan schedule', 'vigilante' ), 'label_en' => 'Scan schedule', 'keywords' => 'escaneo escáner programación planificación horario frecuencia cron' ),
1177 - array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Instant alert', 'vigilante' ), 'label_en' => 'Instant alert', 'keywords' => 'alerta instantánea inmediata notificación aviso email tiempo real' ),
1178 - array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'Ignored Files', 'vigilante' ), 'anchor' => 'vigilante-section-fi-ignored', 'label' => __( 'Ignored Files', 'vigilante' ), 'label_en' => 'Ignored Files', 'keywords' => 'ignorados ignorar excluir exclusiones archivos ignored exclude' ),
1479 + 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' ) ),
1480 + 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' ) ),
1481 + 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' ) ),
1482 + 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' ) ),
1179 1483 // Security Audit
1180 - array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Security Audit Settings', 'vigilante' ), 'anchor' => 'vigilante-section-audit-settings', 'label' => __( 'Retention', 'vigilante' ), 'label_en' => 'Retention', 'keywords' => 'retención días log registro conservación purga auditoría' ),
1181 - array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Security Audit Settings', 'vigilante' ), 'anchor' => 'vigilante-section-audit-settings', 'label' => __( 'Events to Log', 'vigilante' ), 'label_en' => 'Events to Log', 'keywords' => 'eventos log registro auditoría registrar capturar' ),
1182 - array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Security Audit Settings', 'vigilante' ), 'anchor' => 'vigilante-section-audit-settings', 'label' => __( 'Option Tracking', 'vigilante' ), 'label_en' => 'Option Tracking', 'keywords' => 'opciones seguimiento rastreo cambios ajustes options tracking' ),
1183 - array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Security Audit Settings', 'vigilante' ), 'anchor' => 'vigilante-section-audit-settings', 'label' => __( 'Exclusions', 'vigilante' ), 'label_en' => 'Exclusions', 'keywords' => 'exclusiones excluir ignorar usuarios roles ip filtros' ),
1184 - array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'vigilante-section-audit-alerts', 'label' => __( 'Audit Alerts', 'vigilante' ), 'label_en' => 'Audit Alerts', 'keywords' => 'alertas avisos aviso notificaciones email correo mail auditoría audit seguridad warning critical destinatarios test prueba' ),
1185 - array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'field-audit-alerts-immediate', 'label' => __( 'Immediate alerts', 'vigilante' ), 'label_en' => 'Immediate alerts', 'keywords' => 'alertas inmediatas email correo mail aviso severidad crítico critical warning evento auditoría inmediato' ),
1186 - array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'field-audit-alerts-threshold', 'label' => __( 'Threshold alerts', 'vigilante' ), 'label_en' => 'Threshold alerts', 'keywords' => 'alertas umbral pico ráfaga email correo mail aviso categoría ventana threshold auditoría cortafuegos login' ),
1187 - array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Recent Activity', 'vigilante' ), 'anchor' => 'vigilante-section-audit-recent', 'label' => __( 'Recent Activity', 'vigilante' ), 'label_en' => 'Recent Activity', 'keywords' => 'actividad reciente log registro eventos últimos historial' ),
1484 + 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' ) ),
1485 + 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' ) ),
1486 + 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' ) ),
1487 + 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' ) ),
1488 + 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' ) ),
1489 + 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' ) ),
1490 + 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' ) ),
1491 + 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' ) ),
1188 1492 // Settings & Tools
1189 - array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Notification settings', 'vigilante' ), 'anchor' => 'vigilante-section-tools-notifications', 'label' => __( 'Notification settings', 'vigilante' ), 'label_en' => 'Notification settings', 'keywords' => 'notificaciones ajustes settings email correo avisos alertas' ),
1190 - array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Notification settings', 'vigilante' ), 'anchor' => 'vigilante-section-tools-notifications', 'label' => __( 'Additional Recipients', 'vigilante' ), 'label_en' => 'Additional Recipients', 'keywords' => 'destinatarios adicionales correo email cc copia recipients' ),
1191 - array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Tools', 'vigilante' ), 'anchor' => 'vigilante-section-tools-main', 'label' => __( 'Export Settings', 'vigilante' ), 'label_en' => 'Export Settings', 'keywords' => 'exportar export ajustes configuración settings json' ),
1192 - array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Tools', 'vigilante' ), 'anchor' => 'vigilante-section-tools-main', 'label' => __( 'Import Settings', 'vigilante' ), 'label_en' => 'Import Settings', 'keywords' => 'importar import ajustes configuración settings json' ),
1193 - array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Tools', 'vigilante' ), 'anchor' => 'vigilante-section-tools-main', 'label' => __( 'Reset to Defaults', 'vigilante' ), 'label_en' => 'Reset to Defaults', 'keywords' => 'restablecer resetear reset defaults predeterminados valores originales fábrica' ),
1194 - array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Tools', 'vigilante' ), 'anchor' => 'vigilante-section-tools-main', 'label' => __( 'Create Backup', 'vigilante' ), 'label_en' => 'Create Backup', 'keywords' => 'copia seguridad backup crear generar respaldo' ),
1195 - array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Tools', 'vigilante' ), 'anchor' => 'vigilante-section-tools-main', 'label' => __( 'Database Backup', 'vigilante' ), 'label_en' => 'Database Backup', 'keywords' => 'base datos database db mysql copia seguridad backup respaldo tablas exportar' ),
1493 + 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' ) ),
1494 + 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' ) ),
1495 + 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' ) ),
1496 + 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' ) ),
1497 + 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' ) ),
1498 + 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' ) ),
1499 + 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' ) ),
1500 + // Entradas anadidas en la 2.9.7 tras comprobar que el indice cubria 68 de
1501 + // las 131 filas de ajustes: buscar XML-RPC, por ejemplo, no devolvia nada.
1502 + // El indice se mantiene a mano, asi que al anadir una fila de ajustes hay
1503 + // que anadirla tambien aqui.
1504 + 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' ) ),
1505 + 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' ) ),
1506 + 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' ) ),
1507 + 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' ) ),
1508 + 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' ) ),
1509 + 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' ) ),
1510 + 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' ) ),
1511 + 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' ) ),
1512 + 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' ) ),
1513 + 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' ) ),
1514 + 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' ) ),
1515 + 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' ) ),
1516 + 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' ) ),
1517 + 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' ) ),
1518 + 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' ) ),
1519 + 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' ) ),
1520 + 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' ) ),
1521 + 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' ) ),
1522 + 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' ) ),
1523 + 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' ) ),
1524 + 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' ) ),
1525 + 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' ) ),
1526 + 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' ) ),
1527 + 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' ) ),
1528 + 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' ) ),
1529 + 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' ) ),
1530 + 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' ) ),
1531 + 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' ) ),
1532 + 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' ) ),
1533 + 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' ) ),
1534 + 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' ) ),
1535 + 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' ) ),
1536 + 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' ) ),
1537 + 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' ) ),
1538 + 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' ) ),
1539 + 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' ) ),
1540 + 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' ) ),
1541 + 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' ) ),
1542 + 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' ) ),
1543 + 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' ) ),
1544 + 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' ) ),
1545 + 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' ) ),
1546 + 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' ) ),
1547 + 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' ) ),
1548 + 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' ) ),
1549 + 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' ) ),
1550 + 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' ) ),
1551 + 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' ) ),
1552 + 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' ) ),
1553 + 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' ) ),
1554 + 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' ) ),
1555 + 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' ) ),
1556 + 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' ) ),
1557 + 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' ) ),
1558 + 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' ) ),
1559 + 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' ) ),
1560 + 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' ) ),
1561 + 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' ) ),
1562 + 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' ) ),
1563 + 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' ) ),
1564 + 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' ) ),
1565 + 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' ) ),
1566 + 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' ) ),
1567 + 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' ) ),
1568 + 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' ) ),
1569 + 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' ) ),
1570 + 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' ) ),
1571 + 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' ) ),
1572 + 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' ) ),
1573 + 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' ) ),
1574 + 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' ) ),
1575 + 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' ) ),
1576 + 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' ) ),
1577 + 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' ) ),
1578 + 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' ) ),
1579 + 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' ) ),
1580 + 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' ) ),
1581 + 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' ) ),
1582 + 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' ) ),
1583 + 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' ) ),
1584 + 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' ) ),
1585 + 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' ) ),
1586 + 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' ) ),
1587 + 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' ) ),
1588 + 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' ) ),
1589 + 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' ) ),
1590 + 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' ) ),
1591 + 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' ) ),
1592 + 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' ) ),
1593 + 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' ) ),
1594 + 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' ) ),
1595 + 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' ) ),
1596 + 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' ) ),
1597 + 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' ) ),
1598 + 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' ) ),
1599 + 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' ) ),
1600 + 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' ) ),
1601 + 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' ) ),
1602 + 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' ) ),
1603 + 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' ) ),
1604 + 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' ) ),
1196 1605 );
1197 1606 }
1198 1607
1199 1608 /**
@@ -1228,8 +1637,11 @@
1228 1637 'currentUserId' => get_current_user_id(),
1229 1638 'logoutUrl' => wp_logout_url( wp_login_url() ),
1230 1639 'adminUrl' => admin_url( 'admin.php?page=vigilante' ),
1231 1640 'searchIndex' => $this->get_search_index(),
1641 + // The scan repaints this table from JavaScript, so the same gate
1642 + // has to travel with it or half the screen keeps the dead button.
1643 + 'approvalLocked' => $this->critical_approval_locked(),
1232 1644 'underAttack' => array(
1233 1645 'active' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->is_active(),
1234 1646 'remaining' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->get_remaining_time(),
1235 1647 ),
@@ -1296,13 +1708,17 @@
1296 1708 'criticalConfigTitle' => __( 'Critical config files modified', 'vigilante' ),
1297 1709 'criticalConfigDesc' => __( 'These files are common targets for code injection. Review the changes and approve if they are legitimate. Vigilant\'s own blocks are excluded from this check.', 'vigilante' ),
1298 1710 'approve' => __( 'Approve', 'vigilante' ),
1299 1711 'approving' => __( 'Approving...', 'vigilante' ),
1712 + 'approvalLockedNotice' => $this->critical_approval_notice(),
1300 1713 'criticalApproved' => __( 'Change approved. Next scan will use the current state as baseline.', 'vigilante' ),
1301 1714 'reviewChanges' => __( 'Review changes', 'vigilante' ),
1302 1715 'hideChanges' => __( 'Hide changes', 'vigilante' ),
1303 1716 'changes' => __( 'Changes', 'vigilante' ),
1304 1717 'diffUnavailable' => __( 'Diff not available for this file (baseline was created before diff tracking was added). Approve to enable diff on future changes.', 'vigilante' ),
1718 + 'diffNetwork' => __( 'This file belongs to the whole network, so its line changes are only shown to network administrators, on the main site.', 'vigilante' ),
1719 + 'diffRescan' => __( 'Run a new scan to see the line changes of this file.', 'vigilante' ),
1720 + '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' ),
1305 1721 'diffEmpty' => __( 'No line-level changes detected (may be whitespace or reordering).', 'vigilante' ),
1306 1722 'diffLines' => __( 'lines', 'vigilante' ),
1307 1723 // Under Attack mode strings
1308 1724 'underAttackConfirmActivate' => __( 'Activate Under Attack mode? All visitors will see a verification page for the next 4 hours.', 'vigilante' ),
@@ -1358,8 +1774,9 @@
1358 1774 'logType' => __( 'Type', 'vigilante' ),
1359 1775 'logAction' => __( 'Action', 'vigilante' ),
1360 1776 'logSeverity' => __( 'Severity', 'vigilante' ),
1361 1777 'logMessage' => __( 'Message', 'vigilante' ),
1778 + 'logRequestUri' => __( 'Address', 'vigilante' ),
1362 1779 'logClient' => __( 'Client', 'vigilante' ),
1363 1780 'logUser' => __( 'User', 'vigilante' ),
1364 1781 'logIpAddress' => __( 'IP Address', 'vigilante' ),
1365 1782 'logUserAgent' => __( 'User Agent', 'vigilante' ),
@@ -1394,8 +1811,10 @@
1394 1811 /* translators: 1: selected count, 2: human-readable size */
1395 1812 'dbTablesSelected' => __( '%1$d tables selected (%2$s)', 'vigilante' ),
1396 1813 // Settings search strings
1397 1814 'searchNoResults' => __( 'No matching settings found.', 'vigilante' ),
1815 + /* translators: %d: number of results that did not fit in the list. */
1816 + 'searchMoreResults' => __( '%d more results. Refine the search to see them.', 'vigilante' ),
1398 1817 'searchInTab' => __( 'in', 'vigilante' ),
1399 1818 // Modules string
1400 1819 /* translators: 1: enabled count, 2: total count */
1401 1820 'modulesEnabled' => __( '%1$d / %2$d modules enabled', 'vigilante' ),
@@ -1562,8 +1981,21 @@
1562 1981 </p>
1563 1982 <p>
1564 1983 <em><?php esc_html_e( 'Vigilant has applied the Maximum preset plus extra hardening on top of your previous configuration. Any changes you make to Vigilant settings while this mode is active will be reverted when it ends.', 'vigilante' ); ?></em>
1565 1984 </p>
1985 + <?php
1986 + // The cache-bypass rules could not be written (a host where
1987 + // WordPress cannot write files by itself, a held lock, a
1988 + // failed read-back): show them, so they can be added by hand.
1989 + $ua_instance = new Vigilante_Under_Attack( $this->settings, $this->activity_log );
1990 + if ( $ua_instance->cache_rules_missing() ) :
1991 + ?>
1992 + <p>
1993 + <strong><?php esc_html_e( 'The cache-bypass rules could not be written to your .htaccess.', 'vigilante' ); ?></strong>
1994 + <?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' ); ?>
1995 + </p>
1996 + <textarea readonly rows="9" class="large-text code" onclick="this.select();"><?php echo esc_textarea( Vigilante_Under_Attack::get_cache_bypass_block() ); ?></textarea>
1997 + <?php endif; ?>
1566 1998 </div>
1567 1999 <?php
1568 2000 }
1569 2001 }
@@ -1665,9 +2097,9 @@
1665 2097 </h1>
1666 2098 <div class="vigilante-search-wrapper">
1667 2099 <div class="vigilante-search-input-wrap">
1668 2100 <span class="vigilante-search-icon dashicons dashicons-search" aria-hidden="true"></span>
1669 - <input type="search" id="vigilante-settings-search" class="vigilante-settings-search" placeholder="<?php esc_attr_e( 'Search settings…', 'vigilante' ); ?>" autocomplete="off">
2101 + <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">
1670 2102 <span class="vigilante-search-shortcut" aria-hidden="true">/</span>
1671 2103 </div>
1672 2104 <div id="vigilante-settings-search-results" class="vigilante-search-results" hidden role="listbox"></div>
1673 2105 </div>
@@ -1749,8 +2181,204 @@
1749 2181 <?php
1750 2182 }
1751 2183
1752 2184 /**
2185 + * Values to display for a section that this site does not control
2186 + *
2187 + * On a subsite the stored options are its own copy, which nothing acts on:
2188 + * wp-config.php and .htaccess are written from the main site. Painting the
2189 + * local copy describes a configuration that is not running, so a subsite
2190 + * admin sees a box ticked here and the constant absent from the file, or the
2191 + * other way round. Read the main site's values instead, which are the ones in
2192 + * force, and fall back to the local ones if they cannot be read.
2193 + *
2194 + * @since 2.9.8
2195 + *
2196 + * @param string $section Settings section.
2197 + * @return array
2198 + */
2199 + private function get_section_for_display( $section ) {
2200 + $local = $this->settings->get_section( $section );
2201 +
2202 + if ( ! $this->shared_files_locked() ) {
2203 + return $local;
2204 + }
2205 +
2206 + // shared_files_locked() is only true on multisite, where get_blog_option() exists.
2207 + $main = get_blog_option( get_main_site_id(), Vigilante_Settings::OPTION_NAME, array() );
2208 +
2209 + if ( ! is_array( $main ) || empty( $main[ $section ] ) || ! is_array( $main[ $section ] ) ) {
2210 + return $local;
2211 + }
2212 +
2213 + return wp_parse_args( $main[ $section ], $local );
2214 + }
2215 +
2216 + /**
2217 + * Whether the sections that write wp-config.php and .htaccess are read-only here
2218 + *
2219 + * True on a network when this is not the main site, or the user is not a
2220 + * network administrator. See Vigilante_Settings::can_write_shared_files().
2221 + *
2222 + * @since 2.9.8
2223 + *
2224 + * @return bool
2225 + */
2226 + private function shared_files_locked() {
2227 + return ! Vigilante_Settings::can_write_shared_files();
2228 + }
2229 +
2230 + /**
2231 + * Whether this is the main site and the user cannot change what it builds the shared files from
2232 + *
2233 + * See Vigilante_Settings::get_main_site_file_settings(). On a subsite those
2234 + * settings only act on that site, so they are never locked there.
2235 + *
2236 + * @since 2.11.6
2237 + *
2238 + * @return bool
2239 + */
2240 + private function main_site_files_locked() {
2241 + return $this->shared_files_locked() && Vigilante_Settings::owns_shared_files();
2242 + }
2243 +
2244 + /**
2245 + * Sentence added to a bulk change when some settings were left as they were
2246 + *
2247 + * Importing a file, applying a preset and restoring the defaults touch every
2248 + * section at once, so the user is told that the shared file settings did
2249 + * not move.
2250 + *
2251 + * @since 2.11.6
2252 + *
2253 + * @return string Empty when the user can change every setting.
2254 + */
2255 + private function locked_file_settings_message() {
2256 + if ( ! Vigilante_Settings::get_locked_file_settings() ) {
2257 + return '';
2258 + }
2259 +
2260 + return ' ' . __( 'The settings that end up in wp-config.php or .htaccess were left as they were.', 'vigilante' ) . ' ' . Vigilante_Settings::get_shared_files_notice();
2261 + }
2262 +
2263 + /**
2264 + * Print the shared-files notice for a section that cannot be edited here
2265 + *
2266 + * @since 2.9.8
2267 + */
2268 + private function render_shared_files_notice() {
2269 + if ( ! $this->shared_files_locked() ) {
2270 + return;
2271 + }
2272 + ?>
2273 + <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;">
2274 + <p style="margin:0;"><?php echo esc_html( Vigilante_Settings::get_shared_files_notice() ); ?></p>
2275 + </div>
2276 + <?php
2277 + }
2278 +
2279 + /**
2280 + * Acting on another user's account needs permission over that user
2281 + *
2282 + * Since 2.10.3 the handlers behind these tools ask for edit_user over the
2283 + * target, which is the rule WordPress itself applies. On a network the core
2284 + * grants edit_user only to network administrators, so for anybody else these
2285 + * controls do nothing. Better to say so than to paint a button that silently
2286 + * skips every user.
2287 + *
2288 + * @since 2.10.4
2289 + * @return bool
2290 + */
2291 + private function forwarded_chain_readings() {
2292 + // Shown, not decided on: the firewall resolves the address elsewhere.
2293 + $chain = Vigilante_IP_Utils::trusted_forwarded_for();
2294 +
2295 + if ( '' === $chain ) {
2296 + return array();
2297 + }
2298 +
2299 + $public = array();
2300 +
2301 + foreach ( explode( ',', $chain ) as $entry ) {
2302 + $address = Vigilante_IP_Utils::unmap_ipv4( trim( $entry ) );
2303 +
2304 + if ( filter_var( $address, FILTER_VALIDATE_IP ) && ! Vigilante_IP_Utils::is_own_network( $address ) ) {
2305 + $public[] = $address;
2306 + }
2307 + }
2308 +
2309 + if ( count( $public ) < 2 ) {
2310 + return array();
2311 + }
2312 +
2313 + return array(
2314 + 'now' => Vigilante_IP_Utils::client_from_chain( $chain ),
2315 + 'before' => $public[0],
2316 + );
2317 + }
2318 +
2319 + /**
2320 + * Whether the user tools of this screen are out of reach for this user
2321 + *
2322 + * @return bool
2323 + */
2324 + private function user_actions_locked() {
2325 + // On a single site edit_user maps to edit_users, which a custom role with
2326 + // manage_options may lack: since 2.11.8 approving and rejecting a pending
2327 + // registration ask for it, so the buttons have to say so there too.
2328 + return is_multisite() ? ! current_user_can( 'manage_network_users' ) : ! current_user_can( 'edit_users' );
2329 + }
2330 +
2331 + /**
2332 + * Print the notice for user tools that cannot be used from this site
2333 + *
2334 + * @since 2.10.4
2335 + */
2336 + private function render_user_actions_notice() {
2337 + if ( ! $this->user_actions_locked() ) {
2338 + return;
2339 + }
2340 + ?>
2341 + <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;">
2342 + <?php if ( is_multisite() ) : ?>
2343 + <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>
2344 + <?php else : ?>
2345 + <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>
2346 + <?php endif; ?>
2347 + </div>
2348 + <?php
2349 + }
2350 +
2351 + /**
2352 + * Approving a change to the shared config files needs the network
2353 + *
2354 + * Since 2.11.3 the handler behind the Approve button asks for
2355 + * manage_network_options, because the two files it approves, wp-config.php
2356 + * and the root .htaccess, belong to the installation, and so does the
2357 + * record of them. The button, though, went on being painted for everybody,
2358 + * so the administrator of a subsite saw the warning, saw the button,
2359 + * pressed it and got "Permission denied" with no explanation. That is
2360 + * exactly what user_actions_locked() above exists to avoid, one release
2361 + * later and one screen over. Flagged by @calzbert.
2362 + *
2363 + * @since 2.11.4
2364 + * @return bool
2365 + */
2366 + private function critical_approval_locked() {
2367 + return is_multisite() && ! current_user_can( 'manage_network_options' );
2368 + }
2369 +
2370 + /**
2371 + * The line that replaces the Approve button where it cannot be used
2372 + *
2373 + * @since 2.11.4
2374 + * @return string
2375 + */
2376 + private function critical_approval_notice() {
2377 + 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' );
2378 + }
2379 +
2380 + /**
1753 2381 * Check if module is disabled and render warning
1754 2382 *
1755 2383 * @param string $module_key Module key.
1756 2384 * @return bool True if disabled.
@@ -2294,14 +2922,15 @@
2294 2922
2295 2923 <?php $this->render_analyzer_widget( $analyzer_last_scan, $analyzer_history, $analyzer_categories_def, $analyzer_settings ); ?>
2296 2924
2297 2925 <div class="vigilante-modules-grid">
2298 - <h2><?php esc_html_e( 'Security Modules', 'vigilante' ); ?></h2>
2926 + <h2 id="vigilante-section-dashboard-modules"><?php esc_html_e( 'Security Modules', 'vigilante' ); ?></h2>
2299 2927 <p class="description"><?php esc_html_e( 'Enable or disable security modules. Each module controls a tab with detailed settings.', 'vigilante' ); ?></p>
2300 2928 <div class="vigilante-modules-list">
2301 2929 <?php foreach ( $options['modules'] as $module => $enabled ) :
2302 2930 $label = isset( $module_labels[ $module ] ) ? $module_labels[ $module ] : ucwords( str_replace( '_', ' ', $module ) );
2303 2931 $description = isset( $module_descriptions[ $module ] ) ? $module_descriptions[ $module ] : '';
2932 + $vg_module_locked = $this->main_site_files_locked() && in_array( $module, Vigilante_Settings::get_main_site_file_settings()['modules'], true );
2304 2933 ?>
2305 2934 <div class="vigilante-module-item <?php echo $enabled ? 'enabled' : 'disabled'; ?>">
2306 2935 <div class="vigilante-module-header">
2307 2936 <span class="vigilante-module-status"></span>
@@ -2314,8 +2943,9 @@
2314 2943 <input type="checkbox"
2315 2944 name="modules[<?php echo esc_attr( $module ); ?>]"
2316 2945 value="1"
2317 2946 <?php checked( $enabled ); ?>
2947 + <?php disabled( $vg_module_locked ); ?>
2318 2948 aria-label="<?php echo esc_attr( $toggle_label ); ?>"
2319 2949 data-module="<?php echo esc_attr( $module ); ?>">
2320 2950 <span class="vigilante-toggle-slider"></span>
2321 2951 </label>
@@ -2322,8 +2952,11 @@
2322 2952 </div>
2323 2953 <?php if ( $description ) : ?>
2324 2954 <p class="vigilante-module-desc"><?php echo esc_html( $description ); ?></p>
2325 2955 <?php endif; ?>
2956 + <?php if ( $vg_module_locked ) : ?>
2957 + <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>
2958 + <?php endif; ?>
2326 2959 </div>
2327 2960 <?php endforeach; ?>
2328 2961 </div>
2329 2962 </div>
@@ -2355,9 +2988,9 @@
2355 2988 $ua_remaining_hours = floor( $ua_remaining / 3600 );
2356 2989 $ua_remaining_mins = floor( ( $ua_remaining % 3600 ) / 60 );
2357 2990 ?>
2358 2991 <div class="vigilante-preset-card vigilante-under-attack-card <?php echo $ua_active ? 'vigilante-under-attack-active' : ''; ?>">
2359 - <h3>
2992 + <h3 id="vigilante-section-dashboard-under-attack">
2360 2993 <span class="dashicons dashicons-shield"></span>
2361 2994 <?php esc_html_e( 'Under Attack', 'vigilante' ); ?>
2362 2995 </h3>
2363 2996 <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>
@@ -2435,11 +3068,11 @@
2435 3068 </label>
2436 3069 </td>
2437 3070 </tr>
2438 3071 <tr>
2439 - <th scope="row"><?php esc_html_e( 'Additional Recipients', 'vigilante' ); ?></th>
3072 + <th scope="row"><label for="vigilante-f-email-additional-recipients"><?php esc_html_e( 'Additional Recipients', 'vigilante' ); ?></label></th>
2440 3073 <td>
2441 - <textarea name="email[additional_recipients]" rows="3" class="large-text code" placeholder="maintenance@example.com&#10;security@example.com"><?php echo esc_textarea( $additional ); ?></textarea>
3074 + <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>
2442 3075 <p class="description"><?php esc_html_e( 'One email per line.', 'vigilante' ); ?></p>
2443 3076 </td>
2444 3077 </tr>
2445 3078 <tr>
@@ -2614,9 +3247,9 @@
2614 3247
2615 3248 <div class="vigilante-tool-card">
2616 3249 <h3><?php esc_html_e( 'Import Settings', 'vigilante' ); ?></h3>
2617 3250 <p><?php esc_html_e( 'Import settings from a previously exported JSON file.', 'vigilante' ); ?></p>
2618 - <input type="file" id="vigilante-import-file" accept=".json" style="display: none;">
3251 + <input type="file" id="vigilante-import-file" aria-label="<?php esc_attr_e( 'Configuration file to import', 'vigilante' ); ?>" accept=".json" style="display: none;">
2619 3252 <button type="button" class="button vigilante-import-settings">
2620 3253 <?php esc_html_e( 'Import Settings', 'vigilante' ); ?>
2621 3254 </button>
2622 3255 </div>
@@ -2622,9 +3255,9 @@
2622 3255 </div>
2623 3256
2624 3257 <div class="vigilante-tool-card">
2625 3258 <h3><?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?></h3>
2626 - <p><?php esc_html_e( 'Reset all the Vigilant security settings to default values.', 'vigilante' ); ?></p>
3259 + <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>
2627 3260 <button type="button" class="button vigilante-reset-settings" style="color: #a00;">
2628 3261 <?php esc_html_e( 'Reset All Settings', 'vigilante' ); ?>
2629 3262 </button>
2630 3263 </div>
@@ -2631,13 +3264,19 @@
2631 3264
2632 3265 <div class="vigilante-tool-card">
2633 3266 <h3><?php esc_html_e( 'Download Config Backup', 'vigilante' ); ?></h3>
2634 3267 <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>
3268 + <?php if ( $this->shared_files_locked() ) : ?>
3269 + <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>
3270 + <?php else : ?>
2635 3271 <button type="button" class="button vigilante-create-backup">
2636 3272 <?php esc_html_e( 'Download Backup', 'vigilante' ); ?>
2637 3273 </button>
3274 + <?php endif; ?>
2638 3275 </div>
2639 3276
3277 + <?php if ( ! $this->shared_files_locked() ) : ?>
3278 +
2640 3279 <div class="vigilante-tool-card vigilante-tool-card-wide">
2641 3280 <h3><?php esc_html_e( 'Database Backup', 'vigilante' ); ?></h3>
2642 3281 <p><?php esc_html_e( 'Download a backup of your database as a ZIP file. Select which tables to include.', 'vigilante' ); ?></p>
2643 3282 <button type="button" class="button vigilante-db-backup-toggle">
@@ -2676,8 +3315,15 @@
2676 3315 </div>
2677 3316 </div>
2678 3317 </div>
2679 3318 </div>
3319 + <?php else : ?>
3320 + <div class="vigilante-tool-card vigilante-tool-card-wide">
3321 + <h3><?php esc_html_e( 'Database Backup', 'vigilante' ); ?></h3>
3322 + <p><?php esc_html_e( 'Download a backup of your database as a ZIP file. Select which tables to include.', 'vigilante' ); ?></p>
3323 + <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>
3324 + </div>
3325 + <?php endif; ?>
2680 3326 </div>
2681 3327 <?php
2682 3328 }
2683 3329
@@ -2700,14 +3346,21 @@
2700 3346 <?php esc_html_e( 'Full page caching systems that serve cached pages before PHP executes (Varnish, LiteSpeed Cache, NGINX FastCGI Cache, Cloudflare APO) may bypass PHP-level firewall rules for cached requests. The .htaccess rules will still apply on Apache/LiteSpeed servers.', 'vigilante' ); ?>
2701 3347 </p>
2702 3348 </div>
2703 3349
3350 + <?php $vg_main_locked = $this->main_site_files_locked(); ?>
3351 + <?php if ( $vg_main_locked ) : ?>
3352 + <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;">
3353 + <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>
3354 + </div>
3355 + <?php endif; ?>
3356 +
2704 3357 <table class="form-table">
2705 3358 <tr>
2706 3359 <th scope="row"><?php esc_html_e( 'Block Bad Query Strings', 'vigilante' ); ?></th>
2707 3360 <td>
2708 3361 <label>
2709 - <input type="checkbox" name="firewall[block_bad_query_strings]" value="1" <?php checked( ! empty( $options['block_bad_query_strings'] ) ); ?>>
3362 + <input type="checkbox" name="firewall[block_bad_query_strings]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( ! empty( $options['block_bad_query_strings'] ) ); ?>>
2710 3363 <?php esc_html_e( 'Block malicious query string patterns', 'vigilante' ); ?>
2711 3364 </label>
2712 3365 </td>
2713 3366 </tr>
@@ -2750,9 +3403,9 @@
2750 3403 <tr>
2751 3404 <th scope="row"><?php esc_html_e( 'Block Bad Bots', 'vigilante' ); ?></th>
2752 3405 <td>
2753 3406 <label>
2754 - <input type="checkbox" name="firewall[block_bad_bots]" value="1" <?php checked( ! empty( $options['block_bad_bots'] ) ); ?>>
3407 + <input type="checkbox" name="firewall[block_bad_bots]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( ! empty( $options['block_bad_bots'] ) ); ?>>
2755 3408 <?php esc_html_e( 'Block known malicious bots and scanners', 'vigilante' ); ?>
2756 3409 </label>
2757 3410 </td>
2758 3411 </tr>
@@ -2769,11 +3422,11 @@
2769 3422 </label>
2770 3423 </td>
2771 3424 </tr>
2772 3425 <tr>
2773 - <th scope="row"><?php esc_html_e( 'Requests per Minute', 'vigilante' ); ?></th>
3426 + <th scope="row"><label for="vigilante-f-firewall-rate-limiting-requests-per-minute"><?php esc_html_e( 'Requests per Minute', 'vigilante' ); ?></label></th>
2774 3427 <td>
2775 - <input type="number" name="firewall[rate_limiting][requests_per_minute]" value="<?php echo esc_attr( $options['rate_limiting']['requests_per_minute'] ?? 120 ); ?>" min="10" max="500" class="small-text">
3428 + <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">
2776 3429 <p class="description">
2777 3430 <?php esc_html_e( 'Counts only PHP requests to WordPress (pages, admin-ajax, REST, login) from a single IP, not static assets like images, CSS or JS. 120/min suits most sites; sustained traffic above that from one IP is usually a bot. To allow a legitimate service, whitelist its IP instead of raising the limit.', 'vigilante' ); ?>
2778 3431 </p>
2779 3432 </td>
@@ -2778,11 +3431,11 @@
2778 3431 </p>
2779 3432 </td>
2780 3433 </tr>
2781 3434 <tr>
2782 - <th scope="row"><?php esc_html_e( 'Block Duration (seconds)', 'vigilante' ); ?></th>
3435 + <th scope="row"><label for="vigilante-f-firewall-rate-limiting-block-duration"><?php esc_html_e( 'Block Duration (seconds)', 'vigilante' ); ?></label></th>
2783 3436 <td>
2784 - <input type="number" name="firewall[rate_limiting][block_duration]" value="<?php echo esc_attr( $options['rate_limiting']['block_duration'] ?? 300 ); ?>" min="60" max="3600" class="small-text">
3437 + <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">
2785 3438 </td>
2786 3439 </tr>
2787 3440 <tr>
2788 3441 <th scope="row"><?php esc_html_e( 'Progressive Blocking', 'vigilante' ); ?></th>
@@ -2805,11 +3458,11 @@
2805 3458 </p>
2806 3459 </td>
2807 3460 </tr>
2808 3461 <tr>
2809 - <th scope="row"><?php esc_html_e( 'Maximum Block Duration', 'vigilante' ); ?></th>
3462 + <th scope="row"><label for="vigilante-f-firewall-rate-limiting-max-block-duration"><?php esc_html_e( 'Maximum Block Duration', 'vigilante' ); ?></label></th>
2810 3463 <td>
2811 - <select name="firewall[rate_limiting][max_block_duration]">
3464 + <select id="vigilante-f-firewall-rate-limiting-max-block-duration" name="firewall[rate_limiting][max_block_duration]">
2812 3465 <?php
2813 3466 $max_options = array(
2814 3467 3600 => __( '1 hour', 'vigilante' ),
2815 3468 21600 => __( '6 hours', 'vigilante' ),
@@ -2866,8 +3519,29 @@
2866 3519 </table>
2867 3520 </div>
2868 3521 <?php endif; ?>
2869 3522
3523 + <?php
3524 + // Since 2.11.8 X-Forwarded-For is read from its end, where the proxy
3525 + // writes. The administrator's own request shows whether that end is
3526 + // a CDN or a balancer for everybody here. Cross review of 2.11.8.
3527 + $xff_readings = $this->forwarded_chain_readings();
3528 + if ( $xff_readings ) :
3529 + ?>
3530 + <div id="vigilante-xff-chain-notice" class="notice notice-warning inline" style="margin:10px 0 16px;padding:8px 12px;">
3531 + <p style="margin:0;">
3532 + <?php
3533 + printf(
3534 + /* translators: 1: last address in the header, the one Vigilant reads, 2: first address in the header, which a visitor can write */
3535 + 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, and not 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' ),
3536 + esc_html( $xff_readings['now'] ),
3537 + esc_html( $xff_readings['before'] )
3538 + );
3539 + ?>
3540 + </p>
3541 + </div>
3542 + <?php endif; ?>
3543 +
2870 3544 <h3><?php esc_html_e( 'IP Lists', 'vigilante' ); ?></h3>
2871 3545 <p class="description">
2872 3546 <?php
2873 3547 printf(
@@ -2878,12 +3552,12 @@
2878 3552 ?>
2879 3553 </p>
2880 3554 <table class="form-table">
2881 3555 <tr>
2882 - <th scope="row"><?php esc_html_e( 'Visitor IP detection', 'vigilante' ); ?></th>
3556 + <th scope="row"><label for="vigilante-f-firewall-trusted-proxy-header"><?php esc_html_e( 'Visitor IP detection', 'vigilante' ); ?></label></th>
2883 3557 <td>
2884 3558 <?php $proxy_header = $options['trusted_proxy_header'] ?? ''; ?>
2885 - <select name="firewall[trusted_proxy_header]">
3559 + <select id="vigilante-f-firewall-trusted-proxy-header" name="firewall[trusted_proxy_header]" <?php disabled( $vg_main_locked ); ?>>
2886 3560 <option value="" <?php selected( $proxy_header, '' ); ?>><?php esc_html_e( 'Direct connection, only REMOTE_ADDR (recommended)', 'vigilante' ); ?></option>
2887 3561 <option value="cf-connecting-ip" <?php selected( $proxy_header, 'cf-connecting-ip' ); ?>><?php esc_html_e( 'Behind Cloudflare (CF-Connecting-IP)', 'vigilante' ); ?></option>
2888 3562 <option value="x-forwarded-for" <?php selected( $proxy_header, 'x-forwarded-for' ); ?>><?php esc_html_e( 'Behind a reverse proxy or load balancer (X-Forwarded-For)', 'vigilante' ); ?></option>
2889 3563 <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>
@@ -2893,13 +3567,25 @@
2893 3567 </p>
2894 3568 </td>
2895 3569 </tr>
2896 3570 <tr>
2897 - <th scope="row"><?php esc_html_e( 'IP Whitelist', 'vigilante' ); ?></th>
3571 + <th scope="row"><label for="vigilante-f-firewall-trusted-proxies"><?php esc_html_e( 'Trusted proxy IPs', 'vigilante' ); ?></label></th>
2898 3572 <td>
2899 - <textarea name="firewall[ip_whitelist]" rows="4" class="large-text code" placeholder="192.168.1.50&#10;192.168.1.0/24&#10;192.168.1.*"><?php echo esc_textarea( implode( "\n", $options['ip_whitelist'] ?? array() ) ); ?></textarea>
3573 + <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 disabled( $vg_main_locked ); ?>><?php echo esc_textarea( implode( "\n", $options['trusted_proxies'] ?? array() ) ); ?></textarea>
2900 3574 <p class="description">
2901 - <?php esc_html_e( 'One IP per line. These IPs will bypass firewall checks.', 'vigilante' ); ?>
3575 + <?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' ); ?>
3576 + <?php if ( in_array( $proxy_header, array( 'x-forwarded-for', 'x-real-ip' ), true ) && empty( $options['trusted_proxies'] ) ) : ?>
3577 + <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>
3578 + <?php endif; ?>
3579 + </p>
3580 + </td>
3581 + </tr>
3582 + <tr>
3583 + <th scope="row"><label for="vigilante-f-firewall-ip-whitelist"><?php esc_html_e( 'IP Whitelist', 'vigilante' ); ?></label></th>
3584 + <td>
3585 + <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>
3586 + <p class="description">
3587 + <?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' ); ?>
2902 3588 <br>
2903 3589 <?php
2904 3590 printf(
2905 3591 /* translators: 1: opening <code>, 2: closing </code>. Placeholders wrap the IP, CIDR and wildcard examples. */
@@ -2911,11 +3597,11 @@
2911 3597 </p>
2912 3598 </td>
2913 3599 </tr>
2914 3600 <tr>
2915 - <th scope="row"><?php esc_html_e( 'IP Blacklist', 'vigilante' ); ?></th>
3601 + <th scope="row"><label for="vigilante-f-firewall-ip-blacklist"><?php esc_html_e( 'IP Blacklist', 'vigilante' ); ?></label></th>
2916 3602 <td>
2917 - <textarea name="firewall[ip_blacklist]" rows="4" class="large-text code" placeholder="203.0.113.42&#10;203.0.113.0/24&#10;203.0.113.*"><?php echo esc_textarea( implode( "\n", $options['ip_blacklist'] ?? array() ) ); ?></textarea>
3603 + <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>
2918 3604 <p class="description">
2919 3605 <?php esc_html_e( 'One IP per line. These IPs will be blocked immediately.', 'vigilante' ); ?>
2920 3606 <br>
2921 3607 <?php
@@ -2934,18 +3620,18 @@
2934 3620 <h3><?php esc_html_e( 'User-Agent Lists', 'vigilante' ); ?></h3>
2935 3621 <p><?php esc_html_e( 'Partial matching: enter a keyword and any User-Agent containing it will be matched.', 'vigilante' ); ?></p>
2936 3622 <table class="form-table">
2937 3623 <tr>
2938 - <th scope="row"><?php esc_html_e( 'User-Agent Whitelist', 'vigilante' ); ?></th>
3624 + <th scope="row"><label for="vigilante-f-firewall-ua-whitelist"><?php esc_html_e( 'User-Agent Whitelist', 'vigilante' ); ?></label></th>
2939 3625 <td>
2940 - <textarea name="firewall[ua_whitelist]" rows="4" class="large-text code"><?php echo esc_textarea( implode( "\n", $options['ua_whitelist'] ?? array() ) ); ?></textarea>
3626 + <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>
2941 3627 <p class="description"><?php esc_html_e( 'One User-Agent per line. These will bypass all firewall checks. Example: ManageWP, MainWP, UptimeRobot.', 'vigilante' ); ?></p>
2942 3628 </td>
2943 3629 </tr>
2944 3630 <tr>
2945 - <th scope="row"><?php esc_html_e( 'User-Agent Blacklist', 'vigilante' ); ?></th>
3631 + <th scope="row"><label for="vigilante-f-firewall-ua-blacklist"><?php esc_html_e( 'User-Agent Blacklist', 'vigilante' ); ?></label></th>
2946 3632 <td>
2947 - <textarea name="firewall[ua_blacklist]" rows="4" class="large-text code"><?php echo esc_textarea( implode( "\n", $options['ua_blacklist'] ?? array() ) ); ?></textarea>
3633 + <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>
2948 3634 <p class="description"><?php esc_html_e( 'One User-Agent per line. These will be blocked immediately.', 'vigilante' ); ?></p>
2949 3635 </td>
2950 3636 </tr>
2951 3637 </table>
@@ -2950,9 +3636,16 @@
2950 3636 </tr>
2951 3637 </table>
2952 3638 </div>
2953 3639
2954 - <div id="vigilante-section-firewall-server" class="vigilante-settings-section">
3640 + <?php
3641 + $vg_shared_locked = $this->shared_files_locked();
3642 + // Paint what is actually in force, not this site's unused copy.
3643 + $vg_local_options = $options;
3644 + $options = $this->get_section_for_display( 'firewall' );
3645 + ?>
3646 + <?php $this->render_shared_files_notice(); ?>
3647 + <div id="vigilante-section-firewall-server" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
2955 3648 <h2>
2956 3649 <?php esc_html_e( 'Server Protection', 'vigilante' ); ?>
2957 3650 <span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span>
2958 3651 </h2>
@@ -3033,8 +3726,9 @@
3033 3726 </td>
3034 3727 </tr>
3035 3728 </table>
3036 3729 </div>
3730 + <?php $options = $vg_local_options; ?>
3037 3731
3038 3732 <p class="submit vigilante-submit-buttons">
3039 3733 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
3040 3734 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
@@ -3065,18 +3759,18 @@
3065 3759 <p><?php esc_html_e( 'Brute force protection and WordPress login hardening.', 'vigilante' ); ?></p>
3066 3760
3067 3761 <table class="form-table">
3068 3762 <tr id="field-max-attempts">
3069 - <th scope="row"><?php esc_html_e( 'Max Login Attempts', 'vigilante' ); ?></th>
3763 + <th scope="row"><label for="vigilante-f-login-security-max-attempts"><?php esc_html_e( 'Max Login Attempts', 'vigilante' ); ?></label></th>
3070 3764 <td>
3071 - <input type="number" name="login_security[max_attempts]" value="<?php echo esc_attr( $options['max_attempts'] ?? 5 ); ?>" min="1" max="20" class="small-text">
3765 + <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">
3072 3766 <p class="description"><?php esc_html_e( 'Number of failed attempts before lockout.', 'vigilante' ); ?></p>
3073 3767 </td>
3074 3768 </tr>
3075 3769 <tr>
3076 - <th scope="row"><?php esc_html_e( 'Lockout Duration', 'vigilante' ); ?></th>
3770 + <th scope="row"><label for="vigilante-f-login-security-lockout-duration"><?php esc_html_e( 'Lockout Duration', 'vigilante' ); ?></label></th>
3077 3771 <td>
3078 - <input type="number" name="login_security[lockout_duration]" value="<?php echo esc_attr( ( $options['lockout_duration'] ?? 1800 ) / 60 ); ?>" min="1" max="1440" class="small-text">
3772 + <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">
3079 3773 <?php esc_html_e( 'minutes', 'vigilante' ); ?>
3080 3774 </td>
3081 3775 </tr>
3082 3776 <tr>
@@ -3096,28 +3790,9 @@
3096 3790 <?php esc_html_e( 'Show generic error message instead of specific errors', 'vigilante' ); ?>
3097 3791 </label>
3098 3792 </td>
3099 3793 </tr>
3100 - <tr id="field-disable-xmlrpc">
3101 - <th scope="row"><?php esc_html_e( 'Disable XML-RPC', 'vigilante' ); ?></th>
3102 - <td>
3103 - <label>
3104 - <input type="checkbox" name="login_security[disable_xmlrpc]" value="1" <?php checked( ! empty( $options['disable_xmlrpc'] ) ); ?>>
3105 - <?php esc_html_e( 'Completely disable XML-RPC functionality', 'vigilante' ); ?>
3106 - </label>
3107 - </td>
3108 - </tr>
3109 3794 <tr>
3110 - <th scope="row"><?php esc_html_e( 'Disable XML-RPC Pingback', 'vigilante' ); ?></th>
3111 - <td>
3112 - <label>
3113 - <input type="checkbox" name="login_security[disable_xmlrpc_pingback]" value="1" <?php checked( ! empty( $options['disable_xmlrpc_pingback'] ) ); ?>>
3114 - <?php esc_html_e( 'Remove only the pingback methods and keep the rest of XML-RPC working', 'vigilante' ); ?>
3115 - </label>
3116 - <p class="description"><?php esc_html_e( 'Pingbacks are the part abused for distributed attacks. Use this when something still needs XML-RPC, such as the WordPress mobile app or Jetpack.', 'vigilante' ); ?></p>
3117 - </td>
3118 - </tr>
3119 - <tr>
3120 3795 <th scope="row"><?php esc_html_e( 'Disable Application Passwords', 'vigilante' ); ?></th>
3121 3796 <td>
3122 3797 <label>
3123 3798 <input type="checkbox" name="login_security[disable_application_passwords]" value="1" <?php checked( ! empty( $options['disable_application_passwords'] ) ); ?>>
@@ -3144,8 +3819,11 @@
3144 3819 </p>
3145 3820 <p class="description">
3146 3821 <?php esc_html_e( 'Direct access to wp-login.php and wp-admin will return a 404 error for non-logged users.', 'vigilante' ); ?>
3147 3822 </p>
3823 + <p class="description">
3824 + <?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' ); ?>
3825 + </p>
3148 3826 </div>
3149 3827 </td>
3150 3828 </tr>
3151 3829 </table>
@@ -3234,9 +3912,9 @@
3234 3912 $two_factor = $options['two_factor'] ?? array();
3235 3913 $two_factor_enabled = ! empty( $two_factor['enabled'] );
3236 3914 ?>
3237 3915 <div class="vigilante-settings-section vigilante-lockout-section">
3238 - <h2><?php esc_html_e( 'Login Protection Status', 'vigilante' ); ?></h2>
3916 + <h2 id="vigilante-section-login-status"><?php esc_html_e( 'Login Protection Status', 'vigilante' ); ?></h2>
3239 3917
3240 3918 <table class="form-table">
3241 3919 <tr>
3242 3920 <th scope="row"><?php esc_html_e( 'Current settings', 'vigilante' ); ?></th>
@@ -3396,9 +4074,9 @@
3396 4074 $excluded = $two_factor['excluded_users'] ?? array();
3397 4075 $method = $two_factor['method'] ?? 'email';
3398 4076 $grace_days = $two_factor['grace_period_days'] ?? 3;
3399 4077 ?>
3400 - <h3>
4078 + <h3 id="vigilante-section-login-2fa">
3401 4079 <?php esc_html_e( 'Two-Factor Authentication (2FA)', 'vigilante' ); ?>
3402 4080 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
3403 4081 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
3404 4082 </h3>
@@ -3512,11 +4190,11 @@
3512 4190 </tr>
3513 4191
3514 4192 <!-- TOTP-specific: Grace period -->
3515 4193 <tr class="vigilante-2fa-totp-only" <?php echo 'totp' !== $method ? 'style="display:none;"' : ''; ?>>
3516 - <th scope="row"><?php esc_html_e( 'Grace period', 'vigilante' ); ?></th>
4194 + <th scope="row"><label for="vigilante-f-login-security-two-factor-grace-period-days"><?php esc_html_e( 'Grace period', 'vigilante' ); ?></label></th>
3517 4195 <td>
3518 - <input type="number"
4196 + <input id="vigilante-f-login-security-two-factor-grace-period-days" type="number"
3519 4197 name="login_security[two_factor][grace_period_days]"
3520 4198 value="<?php echo esc_attr( $grace_days ); ?>"
3521 4199 min="0" max="30" class="small-text">
3522 4200 <?php esc_html_e( 'days', 'vigilante' ); ?>
@@ -3525,11 +4203,11 @@
3525 4203 </tr>
3526 4204
3527 4205 <!-- Email-specific: Sender name -->
3528 4206 <tr class="vigilante-2fa-email-only" <?php echo 'email' !== $method ? 'style="display:none;"' : ''; ?>>
3529 - <th scope="row"><?php esc_html_e( 'Email sender name', 'vigilante' ); ?></th>
4207 + <th scope="row"><label for="vigilante-f-login-security-two-factor-email-from-name"><?php esc_html_e( 'Email sender name', 'vigilante' ); ?></label></th>
3530 4208 <td>
3531 - <input type="text"
4209 + <input id="vigilante-f-login-security-two-factor-email-from-name" type="text"
3532 4210 name="login_security[two_factor][email_from_name]"
3533 4211 value="<?php echo esc_attr( $two_factor['email_from_name'] ?? '' ); ?>"
3534 4212 class="regular-text vigilante-2fa-email-from"
3535 4213 placeholder="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?>">
@@ -3597,14 +4275,140 @@
3597 4275
3598 4276 /**
3599 4277 * Render security headers tab
3600 4278 */
4279 + /**
4280 + * Offer back the header settings the 2.9.8 migration wiped.
4281 + *
4282 + * Rendered outside the settings form on purpose, so its buttons can never
4283 + * submit it, and only when there is something to actually change. Shows the
4284 + * difference before anything is written: nothing is applied that the owner
4285 + * has not seen first.
4286 + *
4287 + * @since 2.10.0
4288 + */
4289 + private function render_headers_recovery_offer() {
4290 + /*
4291 + * On a network the .htaccess belongs to every site and only the main one
4292 + * writes it, so this is not a decision a subsite gets to make. Its own
4293 + * security_headers options are inert anyway: what the network serves
4294 + * comes from the file the main site owns. Without this gate a subsite
4295 + * administrator was shown a Restore button that could only ever answer
4296 + * with a permission error, which is worse than showing nothing.
4297 + */
4298 + if ( ! Vigilante_Settings::can_write_shared_files() ) {
4299 + return;
4300 + }
4301 +
4302 + if ( ! Vigilante_Htaccess_Recovery::is_available() ) {
4303 + /*
4304 + * Already restored. Offer to take it back for as long as the previous
4305 + * section is still stored: a restore that cannot be undone is a second
4306 + * irreversible change on top of the one being repaired.
4307 + */
4308 + if ( Vigilante_Htaccess_Recovery::has_undo() ) {
4309 + ?>
4310 + <div class="notice notice-info inline" id="vigilante-headers-recovery-undo">
4311 + <p>
4312 + <?php esc_html_e( 'The Security Headers settings were restored from the copy Vigilant had kept of your .htaccess.', 'vigilante' ); ?>
4313 + <button type="button" class="button button-small" id="vigilante-recovery-undo">
4314 + <?php esc_html_e( 'Undo the restore', 'vigilante' ); ?>
4315 + </button>
4316 + </p>
4317 + </div>
4318 + <?php
4319 + }
4320 +
4321 + return;
4322 + }
4323 +
4324 + $rows = Vigilante_Htaccess_Recovery::get_diff( $this->settings );
4325 +
4326 + if ( empty( $rows ) ) {
4327 + return;
4328 + }
4329 +
4330 + $snapshot = Vigilante_Htaccess_Recovery::get_snapshot();
4331 + $taken = isset( $snapshot['time'] ) ? (int) $snapshot['time'] : 0;
4332 + $block = Vigilante_Htaccess_Recovery::get_raw_block();
4333 + ?>
4334 + <div class="vigilante-settings-section" id="vigilante-headers-recovery">
4335 + <h2><?php esc_html_e( 'Recover your previous header settings', 'vigilante' ); ?></h2>
4336 + <p>
4337 + <?php esc_html_e( 'An earlier update 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' ); ?>
4338 + </p>
4339 + <?php if ( $taken ) : ?>
4340 + <p class="description">
4341 + <?php
4342 + printf(
4343 + /* translators: %s: date and time the .htaccess copy was taken. */
4344 + esc_html__( 'Copy taken on %s.', 'vigilante' ),
4345 + esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $taken ) )
4346 + );
4347 + ?>
4348 + </p>
4349 + <?php endif; ?>
4350 +
4351 + <table class="widefat striped">
4352 + <thead>
4353 + <tr>
4354 + <th scope="col"><?php esc_html_e( 'Setting', 'vigilante' ); ?></th>
4355 + <th scope="col"><?php esc_html_e( 'Now', 'vigilante' ); ?></th>
4356 + <th scope="col"><?php esc_html_e( 'Would be restored to', 'vigilante' ); ?></th>
4357 + </tr>
4358 + </thead>
4359 + <tbody>
4360 + <?php foreach ( $rows as $row ) : ?>
4361 + <tr>
4362 + <th scope="row"><?php echo esc_html( $row['label'] ); ?></th>
4363 + <td><?php echo esc_html( $row['current'] ); ?></td>
4364 + <td>
4365 + <?php echo esc_html( $row['recovered'] ); ?>
4366 + <?php if ( ! empty( $row['detail'] ) ) : ?>
4367 + <br><span class="description"><?php echo esc_html( $row['detail'] ); ?></span>
4368 + <?php endif; ?>
4369 + </td>
4370 + </tr>
4371 + <?php endforeach; ?>
4372 + </tbody>
4373 + </table>
4374 +
4375 + <p class="description">
4376 + <?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' ); ?>
4377 + </p>
4378 +
4379 + <?php if ( '' !== $block ) : ?>
4380 + <details>
4381 + <summary><?php esc_html_e( 'Show the saved .htaccess block', 'vigilante' ); ?></summary>
4382 + <textarea readonly rows="12" class="large-text code" onclick="this.select();"><?php echo esc_textarea( $block ); ?></textarea>
4383 + </details>
4384 + <?php endif; ?>
4385 +
4386 + <p class="submit vigilante-submit-buttons">
4387 + <button type="button" class="button button-primary" id="vigilante-recovery-restore">
4388 + <?php esc_html_e( 'Restore these settings', 'vigilante' ); ?>
4389 + </button>
4390 + <button type="button" class="button" id="vigilante-recovery-dismiss">
4391 + <?php esc_html_e( 'No thanks, keep what I have', 'vigilante' ); ?>
4392 + </button>
4393 + </p>
4394 + <div id="vigilante-recovery-result"></div>
4395 + </div>
4396 + <?php
4397 + }
4398 +
3601 4399 private function render_tab_headers() {
3602 4400 $is_disabled = $this->render_module_disabled_notice( 'security_headers' );
3603 - $options = $this->settings->get_section( 'security_headers' );
4401 + // Every setting on this tab ends up in .htaccess, so on a subsite the
4402 + // whole tab is somebody else's, values included.
4403 + $vg_shared_locked = $this->shared_files_locked();
4404 + $options = $this->get_section_for_display( 'security_headers' );
3604 4405 ?>
4406 + <?php $this->render_headers_recovery_offer(); ?>
4407 +
3605 4408 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="security_headers" <?php echo $is_disabled ? 'inert' : ''; ?>>
3606 - <div id="vigilante-section-headers-main" class="vigilante-settings-section">
4409 + <?php $this->render_shared_files_notice(); ?>
4410 + <div id="vigilante-section-headers-main" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
3607 4411 <h2>
3608 4412 <?php esc_html_e( 'Security Headers', 'vigilante' ); ?>
3609 4413 <span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span>
3610 4414 </h2>
@@ -3611,11 +4415,11 @@
3611 4415 <p><?php esc_html_e( 'HTTP headers sent with every response via .htaccess (mod_headers).', 'vigilante' ); ?></p>
3612 4416
3613 4417 <table class="form-table">
3614 4418 <tr>
3615 - <th scope="row"><?php esc_html_e( 'X-Frame-Options', 'vigilante' ); ?></th>
4419 + <th scope="row"><label for="vigilante-f-security-headers-x-frame-options"><?php esc_html_e( 'X-Frame-Options', 'vigilante' ); ?></label></th>
3616 4420 <td>
3617 - <select name="security_headers[x_frame_options]">
4421 + <select id="vigilante-f-security-headers-x-frame-options" name="security_headers[x_frame_options]">
3618 4422 <option value="" <?php selected( empty( $options['x_frame_options'] ) ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option>
3619 4423 <option value="SAMEORIGIN" <?php selected( $options['x_frame_options'] ?? '', 'SAMEORIGIN' ); ?>>SAMEORIGIN</option>
3620 4424 <option value="DENY" <?php selected( $options['x_frame_options'] ?? '', 'DENY' ); ?>>DENY</option>
3621 4425 </select>
@@ -3631,11 +4435,11 @@
3631 4435 </label>
3632 4436 </td>
3633 4437 </tr>
3634 4438 <tr>
3635 - <th scope="row"><?php esc_html_e( 'Referrer-Policy', 'vigilante' ); ?></th>
4439 + <th scope="row"><label for="vigilante-f-security-headers-referrer-policy"><?php esc_html_e( 'Referrer-Policy', 'vigilante' ); ?></label></th>
3636 4440 <td>
3637 - <select name="security_headers[referrer_policy]">
4441 + <select id="vigilante-f-security-headers-referrer-policy" name="security_headers[referrer_policy]">
3638 4442 <option value="" <?php selected( empty( $options['referrer_policy'] ) ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option>
3639 4443 <option value="no-referrer" <?php selected( $options['referrer_policy'] ?? '', 'no-referrer' ); ?>>no-referrer</option>
3640 4444 <option value="strict-origin-when-cross-origin" <?php selected( $options['referrer_policy'] ?? '', 'strict-origin-when-cross-origin' ); ?>>strict-origin-when-cross-origin</option>
3641 4445 <option value="same-origin" <?php selected( $options['referrer_policy'] ?? '', 'same-origin' ); ?>>same-origin</option>
@@ -3643,45 +4447,11 @@
3643 4447 </td>
3644 4448 </tr>
3645 4449 </table>
3646 4450
3647 - <h3><?php esc_html_e( 'HSTS (HTTP Strict Transport Security)', 'vigilante' ); ?></h3>
4451 + <h3 id="vigilante-section-headers-csp"><?php esc_html_e( 'Content Security Policy', 'vigilante' ); ?></h3>
3648 4452 <table class="form-table">
3649 4453 <tr>
3650 - <th scope="row"><?php esc_html_e( 'Enable HSTS', 'vigilante' ); ?></th>
3651 - <td>
3652 - <label>
3653 - <input type="checkbox" name="security_headers[hsts][enabled]" value="1" <?php checked( ! empty( $options['hsts']['enabled'] ) ); ?>>
3654 - <?php esc_html_e( 'Force HTTPS connections', 'vigilante' ); ?>
3655 - </label>
3656 - <p class="description"><?php esc_html_e( '&#9888; Warning: Only enable if your site fully supports HTTPS.', 'vigilante' ); ?></p>
3657 - </td>
3658 - </tr>
3659 - <tr>
3660 - <th scope="row"><?php esc_html_e( 'Max Age', 'vigilante' ); ?></th>
3661 - <td>
3662 - <select name="security_headers[hsts][max_age]">
3663 - <option value="86400" <?php selected( $options['hsts']['max_age'] ?? 31536000, 86400 ); ?>><?php esc_html_e( '1 day (testing)', 'vigilante' ); ?></option>
3664 - <option value="2592000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 2592000 ); ?>><?php esc_html_e( '30 days', 'vigilante' ); ?></option>
3665 - <option value="31536000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 31536000 ); ?>><?php esc_html_e( '1 year (recommended)', 'vigilante' ); ?></option>
3666 - <option value="63072000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 63072000 ); ?>><?php esc_html_e( '2 years', 'vigilante' ); ?></option>
3667 - </select>
3668 - </td>
3669 - </tr>
3670 - <tr>
3671 - <th scope="row"><?php esc_html_e( 'Include Subdomains', 'vigilante' ); ?></th>
3672 - <td>
3673 - <label>
3674 - <input type="checkbox" name="security_headers[hsts][include_subdomains]" value="1" <?php checked( ! empty( $options['hsts']['include_subdomains'] ) ); ?>>
3675 - <?php esc_html_e( 'Apply HSTS to all subdomains', 'vigilante' ); ?>
3676 - </label>
3677 - </td>
3678 - </tr>
3679 - </table>
3680 -
3681 - <h3><?php esc_html_e( 'Content Security Policy', 'vigilante' ); ?></h3>
3682 - <table class="form-table">
3683 - <tr>
3684 4454 <th scope="row"><?php esc_html_e( 'Enable CSP', 'vigilante' ); ?></th>
3685 4455 <td>
3686 4456 <label>
3687 4457 <input type="checkbox" name="security_headers[csp][enabled]" value="1" <?php checked( ! empty( $options['csp']['enabled'] ) ); ?>>
@@ -3699,9 +4469,9 @@
3699 4469 </td>
3700 4470 </tr>
3701 4471 </table>
3702 4472
3703 - <h3><?php esc_html_e( 'HTTPS', 'vigilante' ); ?></h3>
4473 + <h3 id="vigilante-section-headers-force-https"><?php esc_html_e( 'HTTPS', 'vigilante' ); ?></h3>
3704 4474 <p class="description"><?php esc_html_e( 'HTTPS is strongly recommended, but Vigilant will not impose it. Enable only what your site already supports.', 'vigilante' ); ?></p>
3705 4475 <table class="form-table">
3706 4476 <tr>
3707 4477 <th scope="row"><?php esc_html_e( 'Redirect HTTP to HTTPS', 'vigilante' ); ?></th>
@@ -3717,12 +4487,23 @@
3717 4487 <th scope="row"><?php esc_html_e( 'Fix Mixed Content', 'vigilante' ); ?></th>
3718 4488 <td>
3719 4489 <label>
3720 4490 <input type="checkbox" name="security_headers[fix_mixed_content]" value="1" <?php checked( ! empty( $options['fix_mixed_content'] ) ); ?>>
3721 - <?php esc_html_e( 'Rewrite http:// resources to https:// and ask browsers to upgrade the rest', 'vigilante' ); ?>
4491 + <?php esc_html_e( 'Rewrite this site http:// resources to https://', 'vigilante' ); ?>
3722 4492 </label>
4493 + <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>
3723 4494 </td>
3724 4495 </tr>
4496 + <tr id="field-upgrade-insecure-requests">
4497 + <th scope="row"><?php esc_html_e( 'Upgrade Insecure Requests', 'vigilante' ); ?></th>
4498 + <td>
4499 + <label>
4500 + <input type="checkbox" name="security_headers[upgrade_insecure_requests]" value="1" <?php checked( ! empty( $options['upgrade_insecure_requests'] ) ); ?>>
4501 + <?php esc_html_e( 'Ask browsers to upgrade every http:// request to https://', 'vigilante' ); ?>
4502 + </label>
4503 + <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>
4504 + </td>
4505 + </tr>
3725 4506 <tr>
3726 4507 <th scope="row"><?php esc_html_e( 'Rewrite Site Address on Activation', 'vigilante' ); ?></th>
3727 4508 <td>
3728 4509 <label>
@@ -3733,9 +4514,52 @@
3733 4514 </td>
3734 4515 </tr>
3735 4516 </table>
3736 4517
3737 - <h3><?php esc_html_e( 'Server Identity', 'vigilante' ); ?></h3>
4518 + <h3 id="vigilante-section-headers-hsts"><?php esc_html_e( 'HSTS (HTTP Strict Transport Security)', 'vigilante' ); ?></h3>
4519 + <?php $vig_home_https = ( 0 === strpos( (string) get_option( 'home' ), 'https://' ) ); ?>
4520 + <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>
4521 + <?php if ( ! $vig_home_https ) : ?>
4522 + <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>
4523 + <?php endif; ?>
4524 + <table class="form-table">
4525 + <tr>
4526 + <th scope="row"><?php esc_html_e( 'Enable HSTS', 'vigilante' ); ?></th>
4527 + <td>
4528 + <?php if ( ! $vig_home_https ) : ?>
4529 + <?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. */ ?>
4530 + <input type="hidden" name="security_headers[hsts][enabled]" value="<?php echo ! empty( $options['hsts']['enabled'] ) ? '1' : '0'; ?>">
4531 + <?php endif; ?>
4532 + <label>
4533 + <input type="checkbox" name="security_headers[hsts][enabled]" value="1" <?php checked( ! empty( $options['hsts']['enabled'] ) ); ?> <?php disabled( ! $vig_home_https ); ?>>
4534 + <?php esc_html_e( 'Send the Strict-Transport-Security header', 'vigilante' ); ?>
4535 + </label>
4536 + <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>
4537 + </td>
4538 + </tr>
4539 + <tr>
4540 + <th scope="row"><label for="vigilante-f-security-headers-hsts-max-age"><?php esc_html_e( 'Max Age', 'vigilante' ); ?></label></th>
4541 + <td>
4542 + <select id="vigilante-f-security-headers-hsts-max-age" name="security_headers[hsts][max_age]">
4543 + <option value="86400" <?php selected( $options['hsts']['max_age'] ?? 31536000, 86400 ); ?>><?php esc_html_e( '1 day (testing)', 'vigilante' ); ?></option>
4544 + <option value="2592000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 2592000 ); ?>><?php esc_html_e( '30 days', 'vigilante' ); ?></option>
4545 + <option value="31536000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 31536000 ); ?>><?php esc_html_e( '1 year (recommended)', 'vigilante' ); ?></option>
4546 + <option value="63072000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 63072000 ); ?>><?php esc_html_e( '2 years', 'vigilante' ); ?></option>
4547 + </select>
4548 + </td>
4549 + </tr>
4550 + <tr>
4551 + <th scope="row"><?php esc_html_e( 'Include Subdomains', 'vigilante' ); ?></th>
4552 + <td>
4553 + <label>
4554 + <input type="checkbox" name="security_headers[hsts][include_subdomains]" value="1" <?php checked( ! empty( $options['hsts']['include_subdomains'] ) ); ?>>
4555 + <?php esc_html_e( 'Apply HSTS to all subdomains', 'vigilante' ); ?>
4556 + </label>
4557 + </td>
4558 + </tr>
4559 + </table>
4560 +
4561 + <h3 id="vigilante-section-headers-fingerprint"><?php esc_html_e( 'Server Identity', 'vigilante' ); ?></h3>
3738 4562 <p class="description"><?php esc_html_e( 'Hide identifying information that servers expose in responses.', 'vigilante' ); ?></p>
3739 4563 <table class="form-table">
3740 4564 <tr>
3741 4565 <th scope="row"><?php esc_html_e( 'Server Signature', 'vigilante' ); ?></th>
@@ -3757,9 +4581,57 @@
3757 4581 </tr>
3758 4582 </table>
3759 4583 </div>
3760 4584
4585 + <?php $vg_cop = ( isset( $options['cross_origin_policies'] ) && is_array( $options['cross_origin_policies'] ) ) ? $options['cross_origin_policies'] : array(); ?>
4586 + <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' : ''; ?>>
4587 + <h2>
4588 + <?php esc_html_e( 'Cross-Origin Policies', 'vigilante' ); ?>
4589 + <span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span>
4590 + </h2>
4591 + <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>
4592 +
4593 + <table class="form-table">
4594 + <tr>
4595 + <th scope="row"><label for="vigilante-f-security-headers-coop"><?php esc_html_e( 'Cross-Origin-Opener-Policy (COOP)', 'vigilante' ); ?></label></th>
4596 + <td>
4597 + <select id="vigilante-f-security-headers-coop" name="security_headers[cross_origin_policies][opener_policy]">
4598 + <option value="" <?php selected( empty( $vg_cop['opener_policy'] ) ); ?>><?php esc_html_e( 'Disabled (header not sent)', 'vigilante' ); ?></option>
4599 + <option value="unsafe-none" <?php selected( $vg_cop['opener_policy'] ?? '', 'unsafe-none' ); ?>>unsafe-none</option>
4600 + <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>
4601 + <option value="same-origin" <?php selected( $vg_cop['opener_policy'] ?? '', 'same-origin' ); ?>>same-origin</option>
4602 + </select>
4603 + <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>
4604 + </td>
4605 + </tr>
4606 + <tr>
4607 + <th scope="row"><label for="vigilante-f-security-headers-coep"><?php esc_html_e( 'Cross-Origin-Embedder-Policy (COEP)', 'vigilante' ); ?></label></th>
4608 + <td>
4609 + <select id="vigilante-f-security-headers-coep" name="security_headers[cross_origin_policies][embedder_policy]">
4610 + <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>
4611 + <option value="credentialless" <?php selected( $vg_cop['embedder_policy'] ?? '', 'credentialless' ); ?>>credentialless</option>
4612 + <option value="require-corp" <?php selected( $vg_cop['embedder_policy'] ?? '', 'require-corp' ); ?>>require-corp</option>
4613 + </select>
4614 + <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>
4615 + </td>
4616 + </tr>
4617 + <tr>
4618 + <th scope="row"><label for="vigilante-f-security-headers-corp"><?php esc_html_e( 'Cross-Origin-Resource-Policy (CORP)', 'vigilante' ); ?></label></th>
4619 + <td>
4620 + <select id="vigilante-f-security-headers-corp" name="security_headers[cross_origin_policies][resource_policy]">
4621 + <option value="" <?php selected( empty( $vg_cop['resource_policy'] ) ); ?>><?php esc_html_e( 'Disabled (header not sent)', 'vigilante' ); ?></option>
4622 + <option value="same-site" <?php selected( $vg_cop['resource_policy'] ?? '', 'same-site' ); ?>>same-site</option>
4623 + <option value="same-origin" <?php selected( $vg_cop['resource_policy'] ?? '', 'same-origin' ); ?>>same-origin</option>
4624 + <option value="cross-origin" <?php selected( $vg_cop['resource_policy'] ?? '', 'cross-origin' ); ?>><?php esc_html_e( 'cross-origin (recommended)', 'vigilante' ); ?></option>
4625 + </select>
4626 + <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>
4627 + </td>
4628 + </tr>
4629 + </table>
4630 + </div>
4631 +
3761 4632 <p class="submit vigilante-submit-buttons">
4633 + <?php if ( ! $vg_shared_locked ) : ?>
3762 4634 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
3763 4635 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
3764 4636 </button>
3765 4637 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
@@ -3764,8 +4636,10 @@
3764 4636 </button>
3765 4637 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
3766 4638 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
3767 4639 </button>
4640 + <?php endif; ?>
4641 + <?php /* Testing what the server actually sends is read-only and useful from any site of a network. */ ?>
3768 4642 <button type="button" class="button vigilante-test-headers">
3769 4643 <?php esc_html_e( 'Test Headers', 'vigilante' ); ?>
3770 4644 </button>
3771 4645 </p>
@@ -3791,11 +4665,11 @@
3791 4665 <p><?php esc_html_e( 'Control access to WordPress REST API endpoints.', 'vigilante' ); ?></p>
3792 4666
3793 4667 <table class="form-table">
3794 4668 <tr>
3795 - <th scope="row"><?php esc_html_e( 'Access Mode', 'vigilante' ); ?></th>
4669 + <th scope="row"><label for="vigilante-f-rest-api-security-mode"><?php esc_html_e( 'Access Mode', 'vigilante' ); ?></label></th>
3796 4670 <td>
3797 - <select name="rest_api_security[mode]">
4671 + <select id="vigilante-f-rest-api-security-mode" name="rest_api_security[mode]">
3798 4672 <option value="open" <?php selected( $options['mode'] ?? 'selective', 'open' ); ?>><?php esc_html_e( 'Open - Allow all requests', 'vigilante' ); ?></option>
3799 4673 <option value="selective" <?php selected( $options['mode'] ?? 'selective', 'selective' ); ?>><?php esc_html_e( 'Selective - Protect sensitive endpoints', 'vigilante' ); ?></option>
3800 4674 <option value="authenticated_only" <?php selected( $options['mode'] ?? 'selective', 'authenticated_only' ); ?>><?php esc_html_e( 'Authenticated - Require login for all', 'vigilante' ); ?></option>
3801 4675 </select>
@@ -3874,14 +4748,14 @@
3874 4748 <?php
3875 4749 $pw_policy = wp_parse_args(
3876 4750 ( isset( $options['password_policy'] ) && is_array( $options['password_policy'] ) ) ? $options['password_policy'] : array(),
3877 4751 array(
3878 - 'require_uppercase' => true,
3879 - 'require_lowercase' => true,
3880 - 'require_number' => true,
3881 - 'require_special' => true,
4752 + 'require_uppercase' => false,
4753 + 'require_lowercase' => false,
4754 + 'require_number' => false,
4755 + 'require_special' => false,
3882 4756 'block_common' => true,
3883 - 'block_username' => false,
4757 + 'block_username' => true,
3884 4758 'affected_roles' => array(),
3885 4759 )
3886 4760 );
3887 4761 $pw_policy_roles = (array) $pw_policy['affected_roles'];
@@ -3895,11 +4769,11 @@
3895 4769 </label>
3896 4770 </td>
3897 4771 </tr>
3898 4772 <tr>
3899 - <th scope="row"><?php esc_html_e( 'Minimum Password Length', 'vigilante' ); ?></th>
4773 + <th scope="row"><label for="vigilante-f-user-security-min-password-length"><?php esc_html_e( 'Minimum Password Length', 'vigilante' ); ?></label></th>
3900 4774 <td>
3901 - <input type="number" name="user_security[min_password_length]" value="<?php echo esc_attr( $options['min_password_length'] ?? 12 ); ?>" min="6" max="32" class="small-text">
4775 + <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">
3902 4776 <?php esc_html_e( 'characters', 'vigilante' ); ?>
3903 4777 </td>
3904 4778 </tr>
3905 4779 <tr>
@@ -4052,11 +4926,11 @@
4052 4926 <p class="description"><?php esc_html_e( 'Disable on high-traffic sites to avoid email overload.', 'vigilante' ); ?></p>
4053 4927 </td>
4054 4928 </tr>
4055 4929 <tr>
4056 - <th scope="row"><?php esc_html_e( 'Auto-reject After', 'vigilante' ); ?></th>
4930 + <th scope="row"><label for="vigilante-f-user-security-registration-approval-auto-reject-days"><?php esc_html_e( 'Auto-reject After', 'vigilante' ); ?></label></th>
4057 4931 <td>
4058 - <input type="number" name="user_security[registration_approval][auto_reject_days]" value="<?php echo esc_attr( $registration['auto_reject_days'] ?? 0 ); ?>" min="0" max="365" class="small-text">
4932 + <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">
4059 4933 <?php esc_html_e( 'days (0 = never)', 'vigilante' ); ?>
4060 4934 <p class="description"><?php esc_html_e( 'Automatically reject pending registrations after this many days.', 'vigilante' ); ?></p>
4061 4935 </td>
4062 4936 </tr>
@@ -4070,8 +4944,16 @@
4070 4944 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4071 4945 </h2>
4072 4946 <p><?php esc_html_e( 'Limit the number of simultaneous sessions per user.', 'vigilante' ); ?></p>
4073 4947
4948 + <?php if ( Vigilante_User_Security::session_limit_is_network_wide() ) : ?>
4949 + <div class="notice notice-warning inline">
4950 + <p>
4951 + <?php esc_html_e( 'This limit does not apply on a network. WordPress keeps the sessions of an account for the whole network, not per site, so a limit set here would count and close the sessions that person opened on other sites, including an administrator session elsewhere. A network-wide session policy is planned; until then these settings are saved but not enforced.', 'vigilante' ); ?>
4952 + </p>
4953 + </div>
4954 + <?php endif; ?>
4955 +
4074 4956 <table class="form-table">
4075 4957 <tr>
4076 4958 <th scope="row"><?php esc_html_e( 'Enable Session Limits', 'vigilante' ); ?></th>
4077 4959 <td>
@@ -4081,18 +4963,18 @@
4081 4963 </label>
4082 4964 </td>
4083 4965 </tr>
4084 4966 <tr>
4085 - <th scope="row"><?php esc_html_e( 'Maximum Sessions', 'vigilante' ); ?></th>
4967 + <th scope="row"><label for="vigilante-f-user-security-session-limits-max-sessions"><?php esc_html_e( 'Maximum Sessions', 'vigilante' ); ?></label></th>
4086 4968 <td>
4087 - <input type="number" name="user_security[session_limits][max_sessions]" value="<?php echo esc_attr( $session_limits['max_sessions'] ?? 3 ); ?>" min="1" max="10" class="small-text">
4969 + <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">
4088 4970 <?php esc_html_e( 'sessions per user', 'vigilante' ); ?>
4089 4971 </td>
4090 4972 </tr>
4091 4973 <tr>
4092 - <th scope="row"><?php esc_html_e( 'When Limit Exceeded', 'vigilante' ); ?></th>
4974 + <th scope="row"><label for="vigilante-f-user-security-session-limits-behavior"><?php esc_html_e( 'When Limit Exceeded', 'vigilante' ); ?></label></th>
4093 4975 <td>
4094 - <select name="user_security[session_limits][behavior]">
4976 + <select id="vigilante-f-user-security-session-limits-behavior" name="user_security[session_limits][behavior]">
4095 4977 <option value="block_new" <?php selected( ( $session_limits['behavior'] ?? 'close_oldest' ), 'block_new' ); ?>><?php esc_html_e( 'Block new login', 'vigilante' ); ?></option>
4096 4978 <option value="close_oldest" <?php selected( ( $session_limits['behavior'] ?? 'close_oldest' ), 'close_oldest' ); ?>><?php esc_html_e( 'Close oldest session', 'vigilante' ); ?></option>
4097 4979 </select>
4098 4980 <p class="description"><?php esc_html_e( '"Close oldest" is recommended for security - ensures attackers cannot lock out legitimate users.', 'vigilante' ); ?></p>
@@ -4128,27 +5010,27 @@
4128 5010 </label>
4129 5011 </td>
4130 5012 </tr>
4131 5013 <tr>
4132 - <th scope="row"><?php esc_html_e( 'Expire After', 'vigilante' ); ?></th>
5014 + <th scope="row"><label for="vigilante-f-user-security-password-expiration-expire-days"><?php esc_html_e( 'Expire After', 'vigilante' ); ?></label></th>
4133 5015 <td>
4134 - <input type="number" name="user_security[password_expiration][expire_days]" value="<?php echo esc_attr( $password_exp['expire_days'] ?? 90 ); ?>" min="7" max="365" class="small-text">
5016 + <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">
4135 5017 <?php esc_html_e( 'days', 'vigilante' ); ?>
4136 5018 <p class="description"><?php esc_html_e( 'PCI-DSS recommends 90 days.', 'vigilante' ); ?></p>
4137 5019 </td>
4138 5020 </tr>
4139 5021 <tr>
4140 - <th scope="row"><?php esc_html_e( 'Warning Period', 'vigilante' ); ?></th>
5022 + <th scope="row"><label for="vigilante-f-user-security-password-expiration-warning-days"><?php esc_html_e( 'Warning Period', 'vigilante' ); ?></label></th>
4141 5023 <td>
4142 - <input type="number" name="user_security[password_expiration][warning_days]" value="<?php echo esc_attr( $password_exp['warning_days'] ?? 14 ); ?>" min="1" max="30" class="small-text">
5024 + <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">
4143 5025 <?php esc_html_e( 'days before expiration', 'vigilante' ); ?>
4144 5026 <p class="description"><?php esc_html_e( 'Show warning notice this many days before password expires.', 'vigilante' ); ?></p>
4145 5027 </td>
4146 5028 </tr>
4147 5029 <tr>
4148 - <th scope="row"><?php esc_html_e( 'Password History', 'vigilante' ); ?></th>
5030 + <th scope="row"><label for="vigilante-f-user-security-password-expiration-password-history"><?php esc_html_e( 'Password History', 'vigilante' ); ?></label></th>
4149 5031 <td>
4150 - <input type="number" name="user_security[password_expiration][password_history]" value="<?php echo esc_attr( $password_exp['password_history'] ?? 3 ); ?>" min="0" max="24" class="small-text">
5032 + <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">
4151 5033 <?php esc_html_e( 'passwords to remember', 'vigilante' ); ?>
4152 5034 <p class="description"><?php esc_html_e( 'Prevent reusing recent passwords. Set to 0 to disable.', 'vigilante' ); ?></p>
4153 5035 </td>
4154 5036 </tr>
@@ -4230,11 +5112,11 @@
4230 5112 </label>
4231 5113 </td>
4232 5114 </tr>
4233 5115 <tr>
4234 - <th scope="row"><?php esc_html_e( 'Link Expiration', 'vigilante' ); ?></th>
5116 + <th scope="row"><label for="vigilante-f-user-security-email-verification-token-expiry-hours"><?php esc_html_e( 'Link Expiration', 'vigilante' ); ?></label></th>
4235 5117 <td>
4236 - <input type="number" name="user_security[email_verification][token_expiry_hours]" value="<?php echo esc_attr( $email_verify['token_expiry_hours'] ?? 24 ); ?>" min="1" max="168" class="small-text">
5118 + <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">
4237 5119 <?php esc_html_e( 'hours', 'vigilante' ); ?>
4238 5120 </td>
4239 5121 </tr>
4240 5122 <tr>
@@ -4246,11 +5128,11 @@
4246 5128 </label>
4247 5129 </td>
4248 5130 </tr>
4249 5131 <tr>
4250 - <th scope="row"><?php esc_html_e( 'Auto-delete Unverified', 'vigilante' ); ?></th>
5132 + <th scope="row"><label for="vigilante-f-user-security-email-verification-auto-delete-days"><?php esc_html_e( 'Auto-delete Unverified', 'vigilante' ); ?></label></th>
4251 5133 <td>
4252 - <input type="number" name="user_security[email_verification][auto_delete_days]" value="<?php echo esc_attr( $email_verify['auto_delete_days'] ?? 7 ); ?>" min="0" max="365" class="small-text">
5134 + <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">
4253 5135 <?php esc_html_e( 'days (0 = never)', 'vigilante' ); ?>
4254 5136 <p class="description"><?php esc_html_e( 'Automatically delete users who never verify their email.', 'vigilante' ); ?></p>
4255 5137 </td>
4256 5138 </tr>
@@ -4274,8 +5156,11 @@
4274 5156 <h2 class="vigilante-tools-header">
4275 5157 <?php esc_html_e( 'User security tools', 'vigilante' ); ?>
4276 5158 </h2>
4277 5159
5160 + <?php $this->render_user_actions_notice(); ?>
5161 + <?php if ( ! $this->user_actions_locked() ) : ?>
5162 +
4278 5163 <!-- Force Password Reset -->
4279 5164 <div class="vigilante-tool-box">
4280 5165 <h3><?php esc_html_e( 'Force password reset', 'vigilante' ); ?></h3>
4281 5166 <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>
@@ -4408,12 +5293,20 @@
4408 5293 </div>
4409 5294
4410 5295 <!-- Pending Registrations -->
4411 5296 <?php
4412 - $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
5297 + // Enforcement-only: this instance exists to read the queue, and the
5298 + // flag keeps it from registering the module's own hooks a second
5299 + // time. It is not inert, and saying it was would be a false comment:
5300 + // init_enforcement_hooks() does add its three filters again, on top
5301 + // of the ones already registered. They are idempotent (the same
5302 + // methods of an equivalent instance, deciding on the same user meta),
5303 + // so running them twice in an admin request changes nothing, which is
5304 + // why this is accepted rather than worked around.
5305 + $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log, true );
4413 5306 $pending_users = $user_security->get_pending_users();
4414 5307 ?>
4415 - <div class="vigilante-tool-box vigilante-pending-users-section">
5308 + <div id="vigilante-section-users-pending" class="vigilante-tool-box vigilante-pending-users-section">
4416 5309 <h3>
4417 5310 <?php esc_html_e( 'Pending registrations', 'vigilante' ); ?>
4418 5311 <?php if ( count( $pending_users ) > 0 ) : ?>
4419 5312 <span class="vigilante-badge vigilante-badge-warning"><?php echo esc_html( count( $pending_users ) ); ?></span>
@@ -4419,9 +5312,20 @@
4419 5312 <span class="vigilante-badge vigilante-badge-warning"><?php echo esc_html( count( $pending_users ) ); ?></span>
4420 5313 <?php endif; ?>
4421 5314 </h3>
4422 5315
4423 - <?php if ( empty( $registration['enabled'] ) ) : ?>
5316 + <?php
5317 + /*
5318 + * The queue is shown whenever there is somebody in it, even with
5319 + * the feature off. Since 2.11.10 an account already waiting stays
5320 + * blocked when the feature is switched off, which is the point:
5321 + * turning a setting off must not quietly let in people an
5322 + * administrator decided not to approve. But hiding the table then
5323 + * left them locked out with no button anywhere to approve or
5324 + * reject them. Found by the cross review of 2.11.10.
5325 + */
5326 + ?>
5327 + <?php if ( empty( $registration['enabled'] ) && empty( $pending_users ) ) : ?>
4424 5328 <p class="description">
4425 5329 <span class="dashicons dashicons-info" style="color: #72aee6;"></span>
4426 5330 <?php esc_html_e( 'Registration approval is disabled. Enable it in the settings above to require manual approval for new users.', 'vigilante' ); ?>
4427 5331 </p>
@@ -4430,8 +5334,9 @@
4430 5334 <span class="dashicons dashicons-yes-alt"></span>
4431 5335 <p><?php esc_html_e( 'No pending registrations.', 'vigilante' ); ?></p>
4432 5336 </div>
4433 5337 <?php else : ?>
5338 + <?php $this->render_user_actions_notice(); ?>
4434 5339 <table class="wp-list-table widefat fixed striped vigilante-pending-users-table">
4435 5340 <thead>
4436 5341 <tr>
4437 5342 <th><?php esc_html_e( 'User', 'vigilante' ); ?></th>
@@ -4441,9 +5346,9 @@
4441 5346 </tr>
4442 5347 </thead>
4443 5348 <tbody>
4444 5349 <?php foreach ( $pending_users as $pending_user ) :
4445 - $pending_since = get_user_meta( $pending_user->ID, 'vigilante_pending_since', true );
5350 + $pending_since = get_user_meta( $pending_user->ID, Vigilante_User_Security::site_user_meta_key( 'vigilante_pending_since' ), true );
4446 5351 ?>
4447 5352 <tr data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>">
4448 5353 <td>
4449 5354 <?php echo get_avatar( $pending_user->ID, 32 ); ?>
@@ -4460,12 +5365,12 @@
4460 5365 }
4461 5366 ?>
4462 5367 </td>
4463 5368 <td>
4464 - <button type="button" class="button button-small vigilante-approve-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>">
5369 + <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() ); ?>>
4465 5370 <?php esc_html_e( 'Approve', 'vigilante' ); ?>
4466 5371 </button>
4467 - <button type="button" class="button button-small vigilante-reject-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>" style="color: #d63638;">
5372 + <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() ); ?>>
4468 5373 <?php esc_html_e( 'Reject', 'vigilante' ); ?>
4469 5374 </button>
4470 5375 </td>
4471 5376 </tr>
@@ -4587,8 +5492,10 @@
4587 5492 </button>
4588 5493 </p>
4589 5494 </div>
4590 5495 </div>
5496 +
5497 + <?php endif; ?>
4591 5498 </div>
4592 5499 <?php
4593 5500 }
4594 5501
@@ -4600,9 +5507,11 @@
4600 5507 $options = $this->settings->get_section( 'wp_hardening' );
4601 5508 ?>
4602 5509 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="wp_hardening" <?php echo $is_disabled ? 'inert' : ''; ?>>
4603 5510 <!-- Database Hardening (outside form save flow - uses its own AJAX action) -->
4604 - <div id="vigilante-section-hardening-database" class="vigilante-settings-section">
5511 + <?php $vg_shared_locked = $this->shared_files_locked(); ?>
5512 + <?php $this->render_shared_files_notice(); ?>
5513 + <div id="vigilante-section-hardening-database" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
4605 5514 <h2>
4606 5515 <?php esc_html_e( 'Database Hardening', 'vigilante' ); ?>
4607 5516 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
4608 5517 <span class="vigilante-method-badge config"><?php esc_html_e( 'WP-CONFIG', 'vigilante' ); ?></span>
@@ -4614,8 +5523,14 @@
4614 5523 $current_prefix = $db_prefix->get_current_prefix();
4615 5524 $is_default = $db_prefix->is_default_prefix();
4616 5525 ?>
4617 5526
5527 + <?php if ( is_multisite() && ! $vg_shared_locked ) : ?>
5528 + <div class="notice notice-warning inline" style="margin:10px 0 16px;padding:8px 12px;">
5529 + <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>
5530 + </div>
5531 + <?php endif; ?>
5532 +
4618 5533 <table class="form-table">
4619 5534 <tr>
4620 5535 <th scope="row"><?php esc_html_e( 'Current prefix', 'vigilante' ); ?></th>
4621 5536 <td>
@@ -4670,9 +5585,16 @@
4670 5585 </table>
4671 5586 </div>
4672 5587
4673 5588 <!-- wp-config Security -->
4674 - <div id="vigilante-section-hardening-wpconfig" class="vigilante-settings-section">
5589 + <?php
5590 + $vg_shared_locked = $this->shared_files_locked();
5591 + // Paint what is actually in force, not this site's unused copy.
5592 + $vg_local_options = $options;
5593 + $options = $this->get_section_for_display( 'wp_hardening' );
5594 + ?>
5595 + <?php $this->render_shared_files_notice(); ?>
5596 + <div id="vigilante-section-hardening-wpconfig" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
4675 5597 <h2>
4676 5598 <?php esc_html_e( 'wp-config.php Security', 'vigilante' ); ?>
4677 5599 <span class="vigilante-method-badge config"><?php esc_html_e( 'WP-CONFIG', 'vigilante' ); ?></span>
4678 5600 </h2>
@@ -4737,10 +5659,40 @@
4737 5659 </td>
4738 5660 </tr>
4739 5661 </table>
4740 5662 </div>
5663 + <?php $options = $vg_local_options; ?>
4741 5664
4742 5665 <!-- Comment Security -->
5666 + <div id="vigilante-section-hardening-xmlrpc" class="vigilante-settings-section">
5667 + <h2>
5668 + <?php esc_html_e( 'XML-RPC', 'vigilante' ); ?>
5669 + <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5670 + </h2>
5671 + <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>
5672 +
5673 + <table class="form-table">
5674 + <tr id="field-disable-xmlrpc">
5675 + <th scope="row"><label for="vigilante-f-wp-hardening-xmlrpc-mode"><?php esc_html_e( 'XML-RPC access', 'vigilante' ); ?></label></th>
5676 + <td>
5677 + <?php $vig_xmlrpc_mode = Vigilante_Comment_Security::resolve_xmlrpc_mode( $this->settings ); ?>
5678 + <select id="vigilante-f-wp-hardening-xmlrpc-mode" name="wp_hardening[xmlrpc_mode]">
5679 + <option value="none" <?php selected( $vig_xmlrpc_mode, 'none' ); ?>>
5680 + <?php esc_html_e( 'Leave XML-RPC enabled', 'vigilante' ); ?>
5681 + </option>
5682 + <option value="pingback" <?php selected( $vig_xmlrpc_mode, 'pingback' ); ?>>
5683 + <?php esc_html_e( 'Block the pingback methods only', 'vigilante' ); ?>
5684 + </option>
5685 + <option value="full" <?php selected( $vig_xmlrpc_mode, 'full' ); ?>>
5686 + <?php esc_html_e( 'Disable XML-RPC completely (recommended)', 'vigilante' ); ?>
5687 + </option>
5688 + </select>
5689 + <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>
5690 + </td>
5691 + </tr>
5692 + </table>
5693 + </div>
5694 +
4743 5695 <div id="vigilante-section-hardening-comments" class="vigilante-settings-section">
4744 5696 <h2>
4745 5697 <?php esc_html_e( 'Comment Security', 'vigilante' ); ?>
4746 5698 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
@@ -4782,10 +5734,10 @@
4782 5734 <label>
4783 5735 <input type="checkbox" name="wp_hardening[close_old_comments]" value="1" <?php checked( ! empty( $options['close_old_comments'] ) ); ?>>
4784 5736 <?php esc_html_e( 'Automatically close comments on old posts after', 'vigilante' ); ?>
4785 5737 </label>
4786 - <input type="number" name="wp_hardening[close_comments_after_days]" value="<?php echo esc_attr( $options['close_comments_after_days'] ?? 30 ); ?>" min="1" max="365" class="small-text">
4787 - <?php esc_html_e( 'days', 'vigilante' ); ?>
5738 + <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">
5739 + <label for="vigilante-f-wp-hardening-close-comments-after-days"><?php esc_html_e( 'days', 'vigilante' ); ?></label>
4788 5740 </td>
4789 5741 </tr>
4790 5742 <tr>
4791 5743 <th scope="row"><?php esc_html_e( 'Honeypot Protection', 'vigilante' ); ?></th>
@@ -4937,13 +5889,13 @@
4937 5889 <table class="form-table">
4938 5890 <tr>
4939 5891 <th scope="row"><?php esc_html_e( 'Retention', 'vigilante' ); ?></th>
4940 5892 <td>
4941 - <input type="number" name="activity_log[retention_days]" value="<?php echo esc_attr( $options['retention_days'] ?? 30 ); ?>" min="7" max="365" class="small-text">
4942 - <?php esc_html_e( 'days', 'vigilante' ); ?>
5893 + <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">
5894 + <label for="vigilante-f-activity-log-retention-days"><?php esc_html_e( 'days', 'vigilante' ); ?></label>
4943 5895 &nbsp;&nbsp;
4944 - <input type="number" name="activity_log[max_entries]" value="<?php echo esc_attr( $options['max_entries'] ?? 10000 ); ?>" min="100" max="100000" step="100" class="small-text">
4945 - <?php esc_html_e( 'max entries', 'vigilante' ); ?>
5896 + <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">
5897 + <label for="vigilante-f-activity-log-max-entries"><?php esc_html_e( 'max entries', 'vigilante' ); ?></label>
4946 5898 <p class="description"><?php esc_html_e( 'Whichever limit is reached first takes effect. Changes apply immediately on save; daily maintenance also enforces these limits automatically.', 'vigilante' ); ?></p>
4947 5899 </td>
4948 5900 </tr>
4949 5901 <tr>
@@ -4968,14 +5920,14 @@
4968 5920 </div>
4969 5921 </td>
4970 5922 </tr>
4971 5923 <tr>
4972 - <th scope="row"><?php esc_html_e( 'Option Tracking', 'vigilante' ); ?></th>
5924 + <th scope="row"><label for="vigilante-f-activity-log-tracked-options"><?php esc_html_e( 'Option Tracking', 'vigilante' ); ?></label></th>
4973 5925 <td>
4974 5926 <p class="description" style="margin-top:0;"><?php esc_html_e( 'When "WordPress option changes" is enabled, Vigilant tracks ~30 core WordPress settings (site URL, admin email, registration, active plugins, theme, comments, privacy, etc.). Use the field below to track additional options from other plugins.', 'vigilante' ); ?></p>
4975 5927 <br>
4976 5928 <label><?php esc_html_e( 'Additional options to track:', 'vigilante' ); ?></label><br>
4977 - <textarea name="activity_log[tracked_options]" rows="3" cols="50" class="regular-text code" placeholder="woocommerce_&#10;seopress_&#10;wpforms_"><?php echo esc_textarea( implode( "\n", $options['tracked_options'] ?? array() ) ); ?></textarea>
5929 + <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>
4978 5930 <p class="description"><?php esc_html_e( 'One option name per line. Use a trailing underscore to match all options with that prefix (e.g. "woocommerce_" tracks all WooCommerce settings).', 'vigilante' ); ?></p>
4979 5931 </td>
4980 5932 </tr>
4981 5933 <tr>
@@ -4982,15 +5934,15 @@
4982 5934 <th scope="row"><?php esc_html_e( 'Exclusions', 'vigilante' ); ?></th>
4983 5935 <td>
4984 5936 <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:16px; max-width:600px;">
4985 5937 <div>
4986 - <label><?php esc_html_e( 'Excluded user IDs:', 'vigilante' ); ?></label><br>
4987 - <textarea name="activity_log[excluded_users]" rows="3" cols="25"><?php echo esc_textarea( implode( "\n", $options['excluded_users'] ?? array() ) ); ?></textarea>
5938 + <label for="vigilante-f-activity-log-excluded-users"><?php esc_html_e( 'Excluded user IDs:', 'vigilante' ); ?></label><br>
5939 + <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>
4988 5940 <p class="description"><?php esc_html_e( 'One user ID per line. Actions by these users will not be logged.', 'vigilante' ); ?></p>
4989 5941 </div>
4990 5942 <div>
4991 - <label><?php esc_html_e( 'Excluded IPs:', 'vigilante' ); ?></label><br>
4992 - <textarea name="activity_log[excluded_ips]" rows="3" cols="25"><?php echo esc_textarea( implode( "\n", $options['excluded_ips'] ?? array() ) ); ?></textarea>
5943 + <label for="vigilante-f-activity-log-excluded-ips"><?php esc_html_e( 'Excluded IPs:', 'vigilante' ); ?></label><br>
5944 + <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>
4993 5945 <p class="description"><?php esc_html_e( 'One IP per line. Requests from these IPs will not be logged.', 'vigilante' ); ?></p>
4994 5946 </div>
4995 5947 </div>
4996 5948 </td>
@@ -5039,11 +5991,11 @@
5039 5991 <p class="description"><?php esc_html_e( 'Sends one email per event type, then waits for the cooldown below before repeating, so a burst of the same event is a single notice.', 'vigilante' ); ?></p>
5040 5992 </td>
5041 5993 </tr>
5042 5994 <tr>
5043 - <th scope="row"><?php esc_html_e( 'Alert on severity', 'vigilante' ); ?></th>
5995 + <th scope="row"><label for="vigilante-f-audit-alerts-immediate-min-severity"><?php esc_html_e( 'Alert on severity', 'vigilante' ); ?></label></th>
5044 5996 <td>
5045 - <select name="audit_alerts[immediate][min_severity]">
5997 + <select id="vigilante-f-audit-alerts-immediate-min-severity" name="audit_alerts[immediate][min_severity]">
5046 5998 <option value="critical" <?php selected( $alert_severity, 'critical' ); ?>><?php esc_html_e( 'Critical only (recommended)', 'vigilante' ); ?></option>
5047 5999 <option value="warning" <?php selected( $alert_severity, 'warning' ); ?>><?php esc_html_e( 'Warning and Critical', 'vigilante' ); ?></option>
5048 6000 </select>
5049 6001 <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>
@@ -5059,11 +6011,11 @@
5059 6011 <p class="description"><?php esc_html_e( 'Catches an attack in progress, e.g. hundreds of firewall blocks or login failures in an hour.', 'vigilante' ); ?></p>
5060 6012 </td>
5061 6013 </tr>
5062 6014 <tr>
5063 - <th scope="row"><?php esc_html_e( 'Time window', 'vigilante' ); ?></th>
6015 + <th scope="row"><label for="vigilante-f-audit-alerts-threshold-window"><?php esc_html_e( 'Time window', 'vigilante' ); ?></label></th>
5064 6016 <td>
5065 - <select name="audit_alerts[threshold][window]">
6017 + <select id="vigilante-f-audit-alerts-threshold-window" name="audit_alerts[threshold][window]">
5066 6018 <option value="30m" <?php selected( $alert_window, '30m' ); ?>><?php esc_html_e( '30 minutes', 'vigilante' ); ?></option>
5067 6019 <option value="1h" <?php selected( $alert_window, '1h' ); ?>><?php esc_html_e( '1 hour', 'vigilante' ); ?></option>
5068 6020 <option value="6h" <?php selected( $alert_window, '6h' ); ?>><?php esc_html_e( '6 hours', 'vigilante' ); ?></option>
5069 6021 <option value="24h" <?php selected( $alert_window, '24h' ); ?>><?php esc_html_e( '24 hours', 'vigilante' ); ?></option>
@@ -5090,10 +6042,10 @@
5090 6042 </tr>
5091 6043 <tr>
5092 6044 <th scope="row"><?php esc_html_e( "Don't repeat alerts", 'vigilante' ); ?></th>
5093 6045 <td>
5094 - <input type="number" name="audit_alerts[cooldown_minutes]" value="<?php echo esc_attr( isset( $alerts['cooldown_minutes'] ) ? (int) $alerts['cooldown_minutes'] : 60 ); ?>" min="0" max="1440" class="small-text">
5095 - <?php esc_html_e( 'minutes', 'vigilante' ); ?>
6046 + <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">
6047 + <label for="vigilante-f-audit-alerts-cooldown-minutes"><?php esc_html_e( 'minutes', 'vigilante' ); ?></label>
5096 6048 <p class="description"><?php esc_html_e( 'After an alert, Vigilant waits this long before sending another about the same thing: the same event type for immediate alerts, or the same category for threshold alerts. This prevents a flood during a sustained attack. Applies to both alert types above.', 'vigilante' ); ?></p>
5097 6049 </td>
5098 6050 </tr>
5099 6051
@@ -5166,10 +6118,10 @@
5166 6118 $ua_blacklist = $firewall_options['ua_blacklist'] ?? array();
5167 6119 ?>
5168 6120
5169 6121 <div class="vigilante-log-filters">
5170 - <input type="text" id="vigilante-log-search" size="1" placeholder="<?php esc_attr_e( 'Search logs (min. 3 characters)...', 'vigilante' ); ?>" class="vigilante-log-search-input">
5171 - <select id="vigilante-log-type-filter">
6122 + <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">
6123 + <select id="vigilante-log-type-filter" aria-label="<?php esc_attr_e( 'Filter the log by event type', 'vigilante' ); ?>">
5172 6124 <option value=""><?php esc_html_e( 'All Types', 'vigilante' ); ?></option>
5173 6125 <option value="login"><?php esc_html_e( 'Login', 'vigilante' ); ?></option>
5174 6126 <option value="user"><?php esc_html_e( 'User', 'vigilante' ); ?></option>
5175 6127 <option value="content"><?php esc_html_e( 'Content', 'vigilante' ); ?></option>
@@ -5182,15 +6134,15 @@
5182 6134 <option value="file"><?php esc_html_e( 'File', 'vigilante' ); ?></option>
5183 6135 <option value="security"><?php esc_html_e( 'Security', 'vigilante' ); ?></option>
5184 6136 <option value="system"><?php esc_html_e( 'System', 'vigilante' ); ?></option>
5185 6137 </select>
5186 - <select id="vigilante-log-severity-filter">
6138 + <select id="vigilante-log-severity-filter" aria-label="<?php esc_attr_e( 'Filter the log by severity', 'vigilante' ); ?>">
5187 6139 <option value=""><?php esc_html_e( 'All Severities', 'vigilante' ); ?></option>
5188 6140 <option value="info"><?php esc_html_e( 'Info', 'vigilante' ); ?></option>
5189 6141 <option value="warning"><?php esc_html_e( 'Warning', 'vigilante' ); ?></option>
5190 6142 <option value="critical"><?php esc_html_e( 'Critical', 'vigilante' ); ?></option>
5191 6143 </select>
5192 - <select id="vigilante-log-method-filter">
6144 + <select id="vigilante-log-method-filter" aria-label="<?php esc_attr_e( 'Filter the log by HTTP method', 'vigilante' ); ?>">
5193 6145 <option value=""><?php esc_html_e( 'All Methods', 'vigilante' ); ?></option>
5194 6146 <option value="GET">GET</option>
5195 6147 <option value="POST">POST</option>
5196 6148 <option value="PUT">PUT</option>
@@ -5257,8 +6209,9 @@
5257 6209 'user' => (string) ( $log->user_login ?? '' ),
5258 6210 'ip' => $ip_val,
5259 6211 'user_agent' => $ua_val,
5260 6212 'request_method' => (string) $request_method,
6213 + 'request_uri' => Vigilante_Activity_Log::extract_request_uri( $log->extra_data ?? '' ),
5261 6214 'date' => (string) ( $log->created_at ?? '' ),
5262 6215 'severity' => (string) ( $log->severity ?? 'info' ),
5263 6216 'is_ip_whitelisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_whitelist, true ) ),
5264 6217 'is_ip_blacklisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_blacklist, true ) ),
@@ -5311,8 +6264,13 @@
5311 6264 */
5312 6265 private function render_tab_file_integrity() {
5313 6266 $is_disabled = $this->render_module_disabled_notice( 'file_integrity' );
5314 6267 $options = $this->settings->get_section( 'file_integrity' );
6268 + // On the main site of a network the critical-file scan is the network's
6269 + // canary for a change to wp-config.php or the root .htaccess, so a
6270 + // main-site admin without network rights cannot turn it off. Since
6271 + // 2.11.8; see Vigilante_Settings::get_main_site_file_settings().
6272 + $vg_main_locked = $this->main_site_files_locked();
5315 6273 $last_scan = get_option( 'vigilante_last_integrity_scan' );
5316 6274 $last_results = get_option( 'vigilante_last_integrity_results' );
5317 6275 $ignored_files = get_option( 'vigilante_ignored_files', array() );
5318 6276
@@ -5365,11 +6323,11 @@
5365 6323 </label>
5366 6324 </td>
5367 6325 </tr>
5368 6326 <tr>
5369 - <th scope="row"><?php esc_html_e( 'Scan Frequency', 'vigilante' ); ?></th>
6327 + <th scope="row"><label for="vigilante-f-file-integrity-scan-frequency"><?php esc_html_e( 'Scan Frequency', 'vigilante' ); ?></label></th>
5370 6328 <td>
5371 - <select name="file_integrity[scan_frequency]">
6329 + <select id="vigilante-f-file-integrity-scan-frequency" name="file_integrity[scan_frequency]">
5372 6330 <option value="daily" <?php selected( $options['scan_frequency'] ?? 'daily', 'daily' ); ?>><?php esc_html_e( 'Daily', 'vigilante' ); ?></option>
5373 6331 <option value="weekly" <?php selected( $options['scan_frequency'] ?? 'daily', 'weekly' ); ?>><?php esc_html_e( 'Weekly', 'vigilante' ); ?></option>
5374 6332 </select>
5375 6333 </td>
@@ -5374,11 +6332,11 @@
5374 6332 </select>
5375 6333 </td>
5376 6334 </tr>
5377 6335 <tr>
5378 - <th scope="row"><?php esc_html_e( 'Email Notifications', 'vigilante' ); ?></th>
6336 + <th scope="row"><label for="vigilante-f-file-integrity-notify-level"><?php esc_html_e( 'Email Notifications', 'vigilante' ); ?></label></th>
5379 6337 <td>
5380 - <select name="file_integrity[notify_level]">
6338 + <select id="vigilante-f-file-integrity-notify-level" name="file_integrity[notify_level]">
5381 6339 <option value="all" <?php selected( $notify_level, 'all' ); ?>><?php esc_html_e( 'All issues (modified + suspicious)', 'vigilante' ); ?></option>
5382 6340 <option value="suspicious_only" <?php selected( $notify_level, 'suspicious_only' ); ?>><?php esc_html_e( 'Suspicious files only', 'vigilante' ); ?></option>
5383 6341 <option value="disabled" <?php selected( $notify_level, 'disabled' ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option>
5384 6342 </select>
@@ -5438,10 +6396,13 @@
5438 6396 <?php esc_html_e( 'Uploads directory (detect PHP files, double extensions, .htaccess)', 'vigilante' ); ?>
5439 6397 </label>
5440 6398 <br>
5441 6399 <label>
5442 - <input type="checkbox" name="file_integrity[scan_critical_config]" value="1" <?php checked( $options['scan_critical_config'] ?? true ); ?>>
6400 + <input type="checkbox" name="file_integrity[scan_critical_config]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( $options['scan_critical_config'] ?? true ); ?>>
5443 6401 <?php esc_html_e( 'Critical config files (wp-config.php, .htaccess baseline monitoring)', 'vigilante' ); ?>
6402 + <?php if ( $vg_main_locked ) : ?>
6403 + <span class="description" style="display:block;margin-left:24px;"><?php echo esc_html( Vigilante_Settings::get_shared_files_notice() ); ?></span>
6404 + <?php endif; ?>
5444 6405 </label>
5445 6406 <br>
5446 6407 <label>
5447 6408 <input type="checkbox" name="file_integrity[check_closed_plugins]" value="1" <?php checked( $options['check_closed_plugins'] ?? true ); ?>>
@@ -5450,19 +6411,30 @@
5450 6411 </fieldset>
5451 6412 </td>
5452 6413 </tr>
5453 6414 <tr>
5454 - <th scope="row"><?php esc_html_e( 'Excluded Paths', 'vigilante' ); ?></th>
6415 + <th scope="row"><label for="vigilante-f-file-integrity-excluded-paths"><?php esc_html_e( 'Excluded Paths', 'vigilante' ); ?></label></th>
5455 6416 <td>
5456 - <textarea name="file_integrity[excluded_paths]" rows="4" class="large-text code" placeholder="wp-content/cache&#10;wp-content/languages"><?php echo esc_textarea( implode( "\n", $options['excluded_paths'] ?? array() ) ); ?></textarea>
5457 - <p class="description"><?php esc_html_e( 'One path per line (relative to WordPress root). Files within these paths will be skipped during scans.', 'vigilante' ); ?></p>
6417 + <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>
6418 + <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>
5458 6419 </td>
5459 6420 </tr>
5460 6421 <tr>
5461 - <th scope="row"><?php esc_html_e( 'Excluded Extensions', 'vigilante' ); ?></th>
6422 + <th scope="row"><label for="vigilante-f-file-integrity-excluded-extensions"><?php esc_html_e( 'Excluded Extensions', 'vigilante' ); ?></label></th>
5462 6423 <td>
5463 - <textarea name="file_integrity[excluded_extensions]" rows="3" class="large-text code" placeholder=".log&#10;.po&#10;.mo&#10;.pot"><?php echo esc_textarea( implode( "\n", $options['excluded_extensions'] ?? array() ) ); ?></textarea>
5464 - <p class="description"><?php esc_html_e( 'One extension per line (e.g. .log, .po, .mo). Files with these extensions will be skipped. Useful to avoid false positives from translation or log files.', 'vigilante' ); ?></p>
6424 + <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>
6425 + <p class="description">
6426 + <?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' ); ?>
6427 + <br>
6428 + <?php
6429 + printf(
6430 + /* translators: 1: opening <code>, 2: closing </code>. Placeholders wrap the scoped-extension example. */
6431 + 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' ),
6432 + '<code>',
6433 + '</code>'
6434 + ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded.
6435 + ?>
6436 + </p>
5465 6437 </td>
5466 6438 </tr>
5467 6439 </table>
5468 6440 </div>
@@ -5709,9 +6681,15 @@
5709 6681 $crit_diff = $crit_item['diff'] ?? array();
5710 6682 $crit_id = sanitize_html_class( $crit_file );
5711 6683 $added_count = is_array( $crit_diff ) ? count( $crit_diff['added'] ?? array() ) : 0;
5712 6684 $removed_count = is_array( $crit_diff ) ? count( $crit_diff['removed'] ?? array() ) : 0;
5713 - $diff_unavailable = is_array( $crit_diff ) && ! empty( $crit_diff['unavailable'] );
6685 + // The lines of a shared file are for whoever approves it. Results
6686 + // stored before 2.11.8 on the main site still carry them, so the
6687 + // screen asks too, not only the scan that wrote them.
6688 + $diff_network = ( is_array( $crit_diff ) && ! empty( $crit_diff['network'] ) ) || $this->critical_approval_locked();
6689 + $diff_rescan = is_array( $crit_diff ) && ! empty( $crit_diff['rescan'] );
6690 + $diff_redaction = is_array( $crit_diff ) && ! empty( $crit_diff['redaction'] );
6691 + $diff_unavailable = $diff_network || ( is_array( $crit_diff ) && ! empty( $crit_diff['unavailable'] ) );
5714 6692 ?>
5715 6693 <tr>
5716 6694 <td><code style="color: #e36210;"><?php echo esc_html( $crit_file ); ?></code></td>
5717 6695 <td>
@@ -5734,18 +6712,36 @@
5734 6712 <td>
5735 6713 <button type="button" class="button button-small vigilante-toggle-critical-content" data-target="vigilante-critical-content-<?php echo esc_attr( $crit_id ); ?>" data-label-show="<?php esc_attr_e( 'Review changes', 'vigilante' ); ?>" data-label-hide="<?php esc_attr_e( 'Hide changes', 'vigilante' ); ?>">
5736 6714 <?php esc_html_e( 'Review changes', 'vigilante' ); ?>
5737 6715 </button>
5738 - <button type="button" class="button button-small button-primary vigilante-approve-critical-file" data-file="<?php echo esc_attr( $crit_file ); ?>">
5739 - <?php esc_html_e( 'Approve', 'vigilante' ); ?>
5740 - </button>
6716 + <?php if ( $this->critical_approval_locked() ) : ?>
6717 + <span class="description" style="display:block;margin-top:4px;">
6718 + <?php echo esc_html( $this->critical_approval_notice() ); ?>
6719 + </span>
6720 + <?php else : ?>
6721 + <button type="button" class="button button-small button-primary vigilante-approve-critical-file" data-file="<?php echo esc_attr( $crit_file ); ?>">
6722 + <?php esc_html_e( 'Approve', 'vigilante' ); ?>
6723 + </button>
6724 + <?php endif; ?>
5741 6725 </td>
5742 6726 </tr>
5743 6727 <tr id="vigilante-critical-content-<?php echo esc_attr( $crit_id ); ?>" class="vigilante-critical-content-row" style="display:none;">
5744 6728 <td colspan="3" style="padding: 0;">
5745 6729 <div class="vigilante-critical-content" style="max-height: 400px; overflow: auto; background: #fff; padding: 10px; font-size: 12px; line-height: 1.5; font-family: Consolas, Monaco, monospace; border-top: 1px solid #c3c4c7;">
5746 - <?php if ( $diff_unavailable ) : ?>
6730 + <?php if ( $diff_network ) : ?>
5747 6731 <p style="color: #50575e; font-style: italic; margin: 0;">
6732 + <?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' ); ?>
6733 + </p>
6734 + <?php elseif ( $diff_rescan ) : ?>
6735 + <p style="color: #50575e; font-style: italic; margin: 0;">
6736 + <?php esc_html_e( 'Run a new scan to see the line changes of this file.', 'vigilante' ); ?>
6737 + </p>
6738 + <?php elseif ( $diff_redaction ) : ?>
6739 + <p style="color: #50575e; font-style: italic; margin: 0;">
6740 + <?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' ); ?>
6741 + </p>
6742 + <?php elseif ( $diff_unavailable ) : ?>
6743 + <p style="color: #50575e; font-style: italic; margin: 0;">
5748 6744 <?php esc_html_e( 'Diff not available for this file (baseline was created before diff tracking was added). Approve to enable diff on future changes.', 'vigilante' ); ?>
5749 6745 </p>
5750 6746 <?php elseif ( empty( $crit_diff['added'] ) && empty( $crit_diff['removed'] ) ) : ?>
5751 6747 <p style="color: #50575e; font-style: italic; margin: 0;">
@@ -5773,9 +6769,9 @@
5773 6769 <?php endif; ?>
5774 6770
5775 6771 <?php if ( $has_closed ) : ?>
5776 6772 <div class="vigilante-file-list vigilante-closed-plugins">
5777 - <h3 style="color: #d63638;"><?php esc_html_e( 'Closed + Removed Plugins', 'vigilante' ); ?></h3>
6773 + <h3 id="vigilante-section-fi-closed-plugins" style="color: #d63638;"><?php esc_html_e( 'Closed + Removed Plugins', 'vigilante' ); ?></h3>
5778 6774 <p class="description" style="color: #d63638;">
5779 6775 <?php esc_html_e( '&#9888; Warning: These plugins have been closed in the WordPress.org repository. Closures usually indicate malware, security issues, guideline violations, or supply chain attacks. Uninstall and replace as soon as possible.', 'vigilante' ); ?>
5780 6776 </p>
5781 6777 <table class="wp-list-table widefat striped">
@@ -5984,8 +6980,15 @@
5984 6980 if ( ! current_user_can( 'manage_options' ) ) {
5985 6981 wp_die( esc_html__( 'Permission denied.', 'vigilante' ), 403 );
5986 6982 }
5987 6983
6984 + // The archive carries wp-config.php, which a whole network shares. On a
6985 + // network manage_options is held by every subsite administrator, so the
6986 + // same gate the writers use applies here.
6987 + if ( ! Vigilante_Settings::can_write_shared_files() ) {
6988 + wp_die( esc_html( Vigilante_Settings::get_shared_files_notice() ), 403 );
6989 + }
6990 +
5988 6991 $backup_manager = new Vigilante_Backup_Manager();
5989 6992 $result = $backup_manager->stream_files_zip();
5990 6993
5991 6994 // stream_files_zip() exits on success; only a WP_Error returns here.
@@ -6074,8 +7077,30 @@
6074 7077
6075 7078 // Read ONLY saved options from database (not merged with defaults)
6076 7079 $saved_options = get_option( Vigilante_Settings::OPTION_NAME, array() );
6077 7080
7081 + // What is stored before this request changes anything: the shared file
7082 + // settings this user may not change are put back from here (2.11.6).
7083 + $stored_options = $saved_options;
7084 + $locked = Vigilante_Settings::get_locked_file_settings();
7085 +
7086 + if ( isset( $locked[ $section ] ) && true === $locked[ $section ] ) {
7087 + wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
7088 + }
7089 +
7090 + // A module switch is a single key, so refusing says more than a success
7091 + // that changed nothing, and the dashboard puts the toggle back.
7092 + if ( 'modules' === $section && isset( $locked['modules'], $data['modules'] ) && is_array( $locked['modules'] ) && is_array( $data['modules'] ) ) {
7093 + foreach ( array_keys( $data['modules'] ) as $vg_module ) {
7094 + if ( in_array( sanitize_key( $vg_module ), $locked['modules'], true ) ) {
7095 + wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
7096 + }
7097 + }
7098 + }
7099 +
7100 + $rejected_ips = array();
7101 + $rejected_proxies = array();
7102 +
6078 7103 // Handle modules
6079 7104 if ( 'modules' === $section && isset( $data['modules'] ) ) {
6080 7105 if ( ! isset( $saved_options['modules'] ) ) {
6081 7106 $saved_options['modules'] = array();
@@ -6093,9 +7118,16 @@
6093 7118 $current_section = isset( $saved_options[ $section ] ) ? $saved_options[ $section ] : array();
6094 7119
6095 7120 // Process the submitted data
6096 7121 $processed = $this->process_section_data( $data[ $section ], $section_defaults, $current_section );
6097 -
7122 +
7123 + // The IP boxes are free text and, until 2.9.9, whatever was typed
7124 + // went straight into the option. An entry the matcher can never
7125 + // match still sits in a security list looking like protection,
7126 + // so the ones that cannot match are dropped and reported back
7127 + // instead of being stored in silence.
7128 + $rejected_ips = $this->filter_ip_lists( $section, $processed, $rejected_proxies );
7129 +
6098 7130 // Save the processed section
6099 7131 $saved_options[ $section ] = $processed;
6100 7132
6101 7133 // Clear active preset when any section settings change
@@ -6105,8 +7137,10 @@
6105 7137
6106 7138 // Clear cache before saving
6107 7139 wp_cache_delete( Vigilante_Settings::OPTION_NAME, 'options' );
6108 7140
7141 + $saved_options = Vigilante_Settings::keep_locked_file_settings( $saved_options, $stored_options );
7142 +
6109 7143 // Save to database
6110 7144 update_option( Vigilante_Settings::OPTION_NAME, $saved_options );
6111 7145
6112 7146 // Clear the settings cache
@@ -6155,12 +7189,84 @@
6155 7189 $login_url_result['sent']
6156 7190 );
6157 7191 }
6158 7192
7193 + if ( ! empty( $rejected_ips ) ) {
7194 + $message .= ' ' . sprintf(
7195 + /* translators: %s: comma separated list of the entries that were not saved. */
7196 + _n(
7197 + 'This entry is not a valid IP, CIDR range or wildcard, so it was not saved: %s',
7198 + 'These entries are not valid IPs, CIDR ranges or wildcards, so they were not saved: %s',
7199 + count( $rejected_ips ),
7200 + 'vigilante'
7201 + ),
7202 + implode( ', ', array_map( 'esc_html', $rejected_ips ) )
7203 + );
7204 + }
7205 +
7206 + if ( ! empty( $rejected_proxies ) ) {
7207 + $message .= ' ' . sprintf(
7208 + /* translators: %s: comma separated list of the trusted proxy entries that were not saved. */
7209 + _n(
7210 + 'A trusted proxy must be an exact IP or a CIDR range, not a wildcard, so this entry was not saved: %s',
7211 + 'A trusted proxy must be an exact IP or a CIDR range, not a wildcard, so these entries were not saved: %s',
7212 + count( $rejected_proxies ),
7213 + 'vigilante'
7214 + ),
7215 + implode( ', ', array_map( 'esc_html', $rejected_proxies ) )
7216 + );
7217 + }
7218 +
6159 7219 wp_send_json_success( $message );
6160 7220 }
6161 -
7221 +
6162 7222 /**
7223 + * Keep only the IP patterns the matcher can actually match
7224 + *
7225 + * @since 2.9.9
7226 + *
7227 + * @param string $section Section being saved.
7228 + * @param array $processed Section data, edited in place.
7229 + * @return array Entries that were dropped, for the message back to the user.
7230 + */
7231 + private function filter_ip_lists( $section, &$processed, &$rejected_proxies = array() ) {
7232 + $rejected_proxies = array();
7233 +
7234 + // Trusted proxies feed an identity decision, so only exact addresses and
7235 + // CIDR ranges belong there: a wildcard is stripped with its own message,
7236 + // never stored looking effective. The matcher ignores it anyway (see
7237 + // Vigilante_IP_Utils::in_list_ip_or_cidr), this stops it persisting.
7238 + if ( 'firewall' === $section && isset( $processed['trusted_proxies'] ) && is_array( $processed['trusted_proxies'] ) ) {
7239 + $split = Vigilante_IP_Utils::split_list_ip_or_cidr( $processed['trusted_proxies'] );
7240 + $processed['trusted_proxies'] = $split['valid'];
7241 + $rejected_proxies = $split['rejected'];
7242 + }
7243 +
7244 + $lists = array(
7245 + 'firewall' => array( 'ip_whitelist', 'ip_blacklist' ),
7246 + 'login_security' => array( 'ip_whitelist' ),
7247 + );
7248 +
7249 + if ( ! isset( $lists[ $section ] ) ) {
7250 + return array();
7251 + }
7252 +
7253 + $rejected = array();
7254 +
7255 + foreach ( $lists[ $section ] as $key ) {
7256 + if ( ! isset( $processed[ $key ] ) || ! is_array( $processed[ $key ] ) ) {
7257 + continue;
7258 + }
7259 +
7260 + $split = Vigilante_IP_Utils::split_list( $processed[ $key ] );
7261 + $processed[ $key ] = $split['valid'];
7262 + $rejected = array_merge( $rejected, $split['rejected'] );
7263 + }
7264 +
7265 + return array_values( array_unique( $rejected ) );
7266 + }
7267 +
7268 + /**
6163 7269 * Send 2FA enable notifications to users
6164 7270 *
6165 7271 * @return array Result with 'sent' and 'failed' counts.
6166 7272 */
@@ -6482,13 +7588,27 @@
6482 7588
6483 7589 // Sanitize imported data recursively
6484 7590 $imported = map_deep( $imported, 'sanitize_text_field' );
6485 7591
6486 - // Validate structure
6487 - $defaults = $this->settings->get_default_options();
6488 - $merged = array_replace_recursive( $defaults, $imported );
7592 + // Validate structure: only sections and keys of the schema survive, and
7593 + // every value takes the type of its default. Until 2.11.0 this was an
7594 + // array_replace_recursive() of the file over the defaults, so any key in
7595 + // the file, known or not, landed in vigilante_options (S7). Sections
7596 + // the file does not carry keep their defaults; a section it does carry
7597 + // replaces the default one whole, because validate_options() has
7598 + // already filled in whatever the file left out.
7599 + $defaults = $this->settings->get_default_options();
7600 + $validated = $this->settings->validate_options( $imported );
7601 + $merged = $defaults;
6489 7602
7603 + foreach ( $validated as $section => $data ) {
7604 + if ( is_array( $data ) ) {
7605 + $merged[ $section ] = $data;
7606 + }
7607 + }
7608 +
6490 7609 // Save
7610 + $merged = Vigilante_Settings::keep_locked_file_settings( $merged, get_option( Vigilante_Settings::OPTION_NAME, array() ) );
6491 7611 update_option( Vigilante_Settings::OPTION_NAME, $merged );
6492 7612 $this->settings->clear_cache();
6493 7613
6494 7614 // Re-evaluate the active preset marker. The imported config may match
@@ -6511,9 +7631,9 @@
6511 7631 if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) {
6512 7632 wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' );
6513 7633 }
6514 7634
6515 - wp_send_json_success( __( 'Settings imported successfully.', 'vigilante' ) );
7635 + wp_send_json_success( __( 'Settings imported successfully.', 'vigilante' ) . $this->locked_file_settings_message() );
6516 7636 }
6517 7637
6518 7638 /**
6519 7639 * Detect whether a vigilante_options array matches a known preset.
@@ -6616,9 +7736,11 @@
6616 7736 $preset = isset( $_POST['preset'] ) ? sanitize_key( $_POST['preset'] ) : '';
6617 7737
6618 7738 // Handle reset to defaults
6619 7739 if ( 'reset' === $preset ) {
6620 - $defaults = $this->settings->get_default_options();
7740 + $stored_options = get_option( Vigilante_Settings::OPTION_NAME, array() );
7741 + $defaults = Vigilante_Settings::get_defaults_preserving_user_data( $stored_options );
7742 + $defaults = Vigilante_Settings::keep_locked_file_settings( $defaults, $stored_options );
6621 7743 update_option( Vigilante_Settings::OPTION_NAME, $defaults );
6622 7744 $this->settings->clear_cache();
6623 7745
6624 7746 // Clear active preset
@@ -6626,9 +7748,9 @@
6626 7748
6627 7749 // Apply file changes after reset
6628 7750 $this->apply_all_file_changes( $defaults );
6629 7751
6630 - wp_send_json_success( __( 'Settings reset to defaults.', 'vigilante' ) );
7752 + wp_send_json_success( __( 'Settings reset to defaults.', 'vigilante' ) . $this->locked_file_settings_message() );
6631 7753 return;
6632 7754 }
6633 7755
6634 7756 $presets = $this->settings->get_presets();
@@ -6649,13 +7771,14 @@
6649 7771 $current = get_option( Vigilante_Settings::OPTION_NAME, array() );
6650 7772 if ( ! is_array( $current ) ) {
6651 7773 $current = array();
6652 7774 }
6653 - // Make sure all known keys exist before merging — array_replace_recursive
6654 - // does not invent keys that are missing on both sides.
6655 - $current = array_replace_recursive( $this->settings->get_default_options(), $current );
7775 + // Make sure all known keys exist before merging — the merge does not
7776 + // invent keys that are missing on both sides.
7777 + $current = Vigilante_Settings::merge_preset( $this->settings->get_default_options(), $current );
6656 7778
6657 - $merged = array_replace_recursive( $current, $preset_options );
7779 + $merged = Vigilante_Settings::merge_preset( $current, $preset_options );
7780 + $merged = Vigilante_Settings::keep_locked_file_settings( $merged, get_option( Vigilante_Settings::OPTION_NAME, array() ) );
6658 7781
6659 7782 update_option( Vigilante_Settings::OPTION_NAME, $merged );
6660 7783 $this->settings->clear_cache();
6661 7784
@@ -6664,9 +7787,9 @@
6664 7787
6665 7788 // Apply file changes after preset
6666 7789 $this->apply_all_file_changes( $merged );
6667 7790
6668 - wp_send_json_success( __( 'Preset applied successfully.', 'vigilante' ) );
7791 + wp_send_json_success( __( 'Preset applied successfully.', 'vigilante' ) . $this->locked_file_settings_message() );
6669 7792 }
6670 7793
6671 7794 /**
6672 7795 * AJAX: Reset a specific section to defaults
@@ -6683,11 +7806,13 @@
6683 7806 if ( empty( $section ) ) {
6684 7807 wp_send_json_error( __( 'No section specified.', 'vigilante' ) );
6685 7808 }
6686 7809
6687 - // Get current options and defaults
7810 + // Get current options and defaults. get_defaults_preserving_user_data()
7811 + // applies the tweaks a fresh installation gets, so the button and a new
7812 + // install agree, and keeps whatever the owner typed in.
6688 7813 $current_options = $this->settings->get_all_options();
6689 - $defaults = $this->settings->get_default_options();
7814 + $defaults = Vigilante_Settings::get_defaults_preserving_user_data( $current_options );
6690 7815
6691 7816 // Check if section exists in defaults
6692 7817 if ( ! isset( $defaults[ $section ] ) ) {
6693 7818 wp_send_json_error( __( 'Invalid section.', 'vigilante' ) );
@@ -6692,11 +7817,27 @@
6692 7817 if ( ! isset( $defaults[ $section ] ) ) {
6693 7818 wp_send_json_error( __( 'Invalid section.', 'vigilante' ) );
6694 7819 }
6695 7820
6696 - // Reset only this section to defaults
6697 - $current_options[ $section ] = $defaults[ $section ];
7821 + $new_values = $defaults[ $section ];
6698 7822
7823 + /*
7824 + * On a subsite, the settings written to wp-config.php and .htaccess are
7825 + * the main site's business. Resetting the local copy of those would only
7826 + * make this screen disagree with the file, so they are carried over
7827 + * untouched, and a section that is nothing but shared settings is not
7828 + * reset at all. On the main site, a user without network rights keeps
7829 + * the ones the shared files are built from as well (2.11.6).
7830 + */
7831 + $locked = Vigilante_Settings::get_locked_file_settings();
7832 +
7833 + if ( isset( $locked[ $section ] ) && true === $locked[ $section ] ) {
7834 + wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
7835 + }
7836 +
7837 + $current_options[ $section ] = $new_values;
7838 + $current_options = Vigilante_Settings::keep_locked_file_settings( $current_options, get_option( Vigilante_Settings::OPTION_NAME, array() ) );
7839 +
6699 7840 // Save
6700 7841 update_option( Vigilante_Settings::OPTION_NAME, $current_options );
6701 7842 $this->settings->clear_cache();
6702 7843
@@ -6778,8 +7919,19 @@
6778 7919 // Save new results
6779 7920 update_option( 'vigilante_last_integrity_scan', time() );
6780 7921 update_option( 'vigilante_last_integrity_results', $results );
6781 7922
7923 + // On the main site the scan does compute the lines of wp-config.php and
7924 + // .htaccess, for the network administrator. Somebody without network
7925 + // rights gets the change and its sizes, not the lines.
7926 + if ( $this->critical_approval_locked() && ! empty( $results['modified'] ) && is_array( $results['modified'] ) ) {
7927 + foreach ( $results['modified'] as $index => $item ) {
7928 + if ( is_array( $item ) && 'critical_config' === ( $item['type'] ?? '' ) ) {
7929 + $results['modified'][ $index ]['diff'] = Vigilante_File_Integrity::network_only_diff();
7930 + }
7931 + }
7932 + }
7933 +
6782 7934 wp_send_json_success( array(
6783 7935 'message' => __( 'Scan completed.', 'vigilante' ),
6784 7936 'results' => $results,
6785 7937 'ignored_count' => count( get_option( 'vigilante_ignored_files', array() ) ),
@@ -6813,11 +7965,41 @@
6813 7965 if ( ! current_user_can( 'manage_options' ) ) {
6814 7966 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6815 7967 }
6816 7968
7969 + $results = get_option( 'vigilante_last_integrity_results' );
7970 + $scanned_at = get_option( 'vigilante_last_integrity_scan' );
7971 +
6817 7972 delete_option( 'vigilante_last_integrity_results' );
6818 7973 delete_option( 'vigilante_last_integrity_scan' );
6819 7974
7975 + /*
7976 + * A pending change to wp-config.php or the root .htaccess is closed by
7977 + * approving it, which takes the network. Clearing the results was one
7978 + * more way to close it without, until the next scan: the ignore list was
7979 + * shut in 2.11.8 and this button was left open, found by the cross
7980 + * review of 2.11.8. So for somebody who cannot approve, those entries
7981 + * stay and everything else goes.
7982 + */
7983 + if ( $this->critical_approval_locked() && is_array( $results ) && ! empty( $results['modified'] ) && is_array( $results['modified'] ) ) {
7984 + $critical = array_values(
7985 + array_filter(
7986 + $results['modified'],
7987 + function ( $item ) {
7988 + return is_array( $item ) && 'critical_config' === ( $item['type'] ?? '' );
7989 + }
7990 + )
7991 + );
7992 +
7993 + if ( $critical ) {
7994 + $results['modified'] = $critical;
7995 + $results['suspicious'] = array();
7996 + $results['extra'] = array();
7997 + update_option( 'vigilante_last_integrity_results', $results );
7998 + update_option( 'vigilante_last_integrity_scan', $scanned_at ? $scanned_at : time() );
7999 + }
8000 + }
8001 +
6820 8002 if ( $this->database ) {
6821 8003 $this->database->clear_file_hashes();
6822 8004 }
6823 8005
@@ -6843,8 +8025,14 @@
6843 8025 if ( empty( $file ) ) {
6844 8026 wp_send_json_error( __( 'No file specified.', 'vigilante' ) );
6845 8027 }
6846 8028
8029 + // A change to a shared file is closed by approving it, and approving it
8030 + // takes the network. Ignoring it would close the same warning without.
8031 + if ( $this->critical_approval_locked() && in_array( $file, array( 'wp-config.php', '.htaccess' ), true ) ) {
8032 + wp_send_json_error( $this->critical_approval_notice() );
8033 + }
8034 +
6847 8035 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
6848 8036 $file_integrity->ignore_file( $file );
6849 8037
6850 8038 // Also remove the file from stored scan results so UI updates
@@ -6908,12 +8096,14 @@
6908 8096 if ( ! is_array( $raw_files ) ) {
6909 8097 wp_send_json_error( __( 'Invalid request.', 'vigilante' ) );
6910 8098 }
6911 8099
6912 - $files = array();
8100 + $files = array();
8101 + $shared = $this->critical_approval_locked() ? array( 'wp-config.php', '.htaccess' ) : array();
6913 8102 foreach ( $raw_files as $f ) {
6914 8103 $clean = sanitize_text_field( $f );
6915 - if ( '' !== $clean ) {
8104 + // Same rule as ajax_ignore_file() for the two shared files.
8105 + if ( '' !== $clean && ! in_array( $clean, $shared, true ) ) {
6916 8106 $files[] = $clean;
6917 8107 }
6918 8108 }
6919 8109