PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 3.0.0
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v3.0.0
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 +2280 -256 2.9.53.0.0 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 *
@@ -106,8 +107,11 @@
106 107 add_action( 'admin_init', array( $this, 'redirect_submenu_shortcuts' ) );
107 108 add_action( 'admin_init', array( $this, 'register_settings' ) );
108 109 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
109 110 add_action( 'admin_notices', array( $this, 'show_admin_notices' ) );
111 + // Self-protection speaks in the network admin too: on a network its
112 + // files are shared, and the super administrator is who can repair them.
113 + add_action( 'network_admin_notices', array( $this, 'maybe_show_self_protection_notice' ) );
110 114
111 115 // Highlight correct submenu based on active tab
112 116 add_filter( 'submenu_file', array( $this, 'highlight_submenu_tab' ) );
113 117
@@ -182,8 +186,13 @@
182 186 add_action( 'wp_ajax_vigilante_analyzer_history', array( $this, 'ajax_analyzer_history' ) );
183 187 add_action( 'wp_ajax_vigilante_analyzer_dismiss_notice', array( $this, 'ajax_analyzer_dismiss_notice' ) );
184 188 add_action( 'wp_ajax_vigilante_analyzer_save_settings', array( $this, 'ajax_analyzer_save_settings' ) );
185 189
190 + // Security Headers settings recovery (2.10.0)
191 + add_action( 'wp_ajax_vigilante_headers_recovery_restore', array( $this, 'ajax_headers_recovery_restore' ) );
192 + add_action( 'wp_ajax_vigilante_headers_recovery_undo', array( $this, 'ajax_headers_recovery_undo' ) );
193 + add_action( 'wp_ajax_vigilante_headers_recovery_dismiss', array( $this, 'ajax_headers_recovery_dismiss' ) );
194 +
186 195 // Shared "Send test email" handler — Notification settings, File Integrity, Audit Alerts (v2.8.0)
187 196 add_action( 'wp_ajax_vigilante_send_test_email', array( $this, 'ajax_send_test_email' ) );
188 197
189 198 // Run migrations on admin load
@@ -193,8 +202,26 @@
193 202 /**
194 203 * Run database migrations based on stored version
195 204 */
196 205 public function run_migrations() {
206 + /*
207 + * admin-ajax.php fires admin_init before it decides who is asking
208 + * (wp-admin/admin-ajax.php:45), so until 2.11.10 an anonymous POST to
209 + * admin-ajax.php with any action ran every pending migration. That is
210 + * not a read: the migrations rewrite wp-config.php through
211 + * apply_security_constants(), rewrite the root .htaccess, move user meta
212 + * of the whole network and can rebuild the file integrity baseline,
213 + * taking whatever is on disk as approved. Reproduced on 12 sep 2026 with
214 + * curl and no cookies, and found by the file-by-file review of 2.11.10.
215 + *
216 + * Migrations are maintenance for whoever administers the site, so they
217 + * wait for an administrator to load a screen. Nothing is lost by
218 + * waiting: every migration is idempotent and version gated.
219 + */
220 + if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) {
221 + return;
222 + }
223 +
197 224 $db_version = get_option( 'vigilante_db_version', '0' );
198 225
199 226 // 1.2.3: Fix IP lists corrupted by sanitize_text_field stripping newlines
200 227 if ( version_compare( $db_version, '1.2.3', '<' ) ) {
@@ -267,9 +294,29 @@
267 294 if ( ! class_exists( 'Vigilante_File_Integrity' ) ) {
268 295 require_once VIGILANTE_INCLUDES_DIR . 'class-file-integrity.php';
269 296 }
270 297 $fi = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log );
271 - $fi->regenerate_all_baselines();
298 +
299 + /*
300 + * Only when there is nothing on record. This migration exists to
301 + * create the baseline that did not exist, never to discard the one
302 + * the owner approved: rebuilding it from the files takes whatever
303 + * is on disk right now as approved, so a wp-config.php modified and
304 + * awaiting review would be blessed in silence.
305 + *
306 + * And this is not theory. vigilante_db_version is written on two
307 + * different scales into the same option: this file counts in plugin
308 + * versions (2.11.0) and Vigilante_Database counts in schema
309 + * versions, currently 1.4.0 (class-database.php:322 and :380). For
310 + * version_compare, 1.4.0 is LOWER than 1.14.0, so any site whose
311 + * option was last written by the schema runs this migration again.
312 + * Measured on the Multisite install on 10 sep 2026: one of the three
313 + * sites was sitting on 1.4.0.
314 + */
315 + if ( ! $fi->get_critical_files_baseline() ) {
316 + $fi->regenerate_all_baselines();
317 + }
318 +
272 319 update_option( 'vigilante_db_version', '1.14.0' );
273 320 }
274 321
275 322 // 2.0.0: Move hide_server_signature and remove_fingerprinting_headers
@@ -343,11 +390,308 @@
343 390 }
344 391
345 392 update_option( 'vigilante_db_version', '2.9.3' );
346 393 }
394 +
395 + /*
396 + * 2.9.8: the mixed content handling changes shape. "Upgrade Insecure
397 + * Requests" becomes a setting of its own, and Fix Mixed Content ships
398 + * off, where before it shipped on and carried the directive with it.
399 + * Both have to be written down for sites that are updating, so their
400 + * pages keep loading exactly what they loaded yesterday.
401 + *
402 + * Read the RAW stored options, not get_section(): that one merges the
403 + * defaults, so a site that never stored the key would be read with the
404 + * new default and silently lose the behaviour it had. Absent means the
405 + * site was running on the old default, which was on.
406 + */
407 + if ( version_compare( $db_version, '2.9.8', '<' ) ) {
408 + $raw = get_option( Vigilante_Settings::OPTION_NAME, array() );
409 + $stored = ( is_array( $raw ) && isset( $raw['security_headers'] ) && is_array( $raw['security_headers'] ) ) ? $raw['security_headers'] : array();
410 + $had_fix = array_key_exists( 'fix_mixed_content', $stored ) ? ! empty( $stored['fix_mixed_content'] ) : true;
411 +
412 + /*
413 + * Merge, never replace. update_section() overwrites the whole
414 + * section, so passing just these two keys wiped every other header
415 + * setting the site had stored (HSTS, CSP, cross-origin policies,
416 + * the HTTPS switches, Server Identity) and left the screen showing
417 + * factory defaults while the .htaccess kept serving the old values.
418 + */
419 + $this->settings->update_section(
420 + 'security_headers',
421 + array_merge(
422 + $stored,
423 + array(
424 + 'fix_mixed_content' => $had_fix,
425 + 'upgrade_insecure_requests' => $had_fix,
426 + )
427 + )
428 + );
429 +
430 + update_option( 'vigilante_db_version', '2.9.8' );
431 + }
432 +
433 + /*
434 + * 2.9.9: drop the settings that no code has read for versions.
435 + *
436 + * They were carried in the defaults and therefore written into every
437 + * saved configuration, they show up in an exported configuration, and
438 + * anyone reading them assumes a feature exists behind them. Removing
439 + * them from the defaults is not enough: the stored copies survive, so
440 + * they are swept here too. Nothing reads them, so nothing changes.
441 + */
442 + if ( version_compare( $db_version, '2.9.9', '<' ) ) {
443 + $raw = get_option( Vigilante_Settings::OPTION_NAME, array() );
444 + $dead = array(
445 + 'firewall' => array( 'country_blocking', 'protected_file_extensions' ),
446 + 'file_integrity' => array( 'suspicious_patterns' ),
447 + 'backup' => array( 'auto_backup', 'backup_before_update' ),
448 + 'advanced' => array( 'block_author_archives', 'disable_embeds', 'uninstall_cleanup', 'debug_mode' ),
449 + );
450 +
451 + $changed = false;
452 + foreach ( $dead as $section => $keys ) {
453 + if ( ! isset( $raw[ $section ] ) || ! is_array( $raw[ $section ] ) ) {
454 + continue;
455 + }
456 + foreach ( $keys as $key ) {
457 + if ( array_key_exists( $key, $raw[ $section ] ) ) {
458 + unset( $raw[ $section ][ $key ] );
459 + $changed = true;
460 + }
461 + }
462 + }
463 +
464 + if ( $changed ) {
465 + update_option( Vigilante_Settings::OPTION_NAME, $raw );
466 + }
467 +
468 + update_option( 'vigilante_db_version', '2.9.9' );
469 + }
470 +
471 + /*
472 + * 2.11.0: security release (audit of 28 Aug 2026). Runs here and not
473 + * from Vigilante_Database::needs_update(): this option is shared with
474 + * that class, and on any updated site it already holds a plugin version
475 + * (2.9.9 or later), so a bump of DB_VERSION would never fire.
476 + * create_tables() widens the email code column through dbDelta (varchar
477 + * 6 to 64, the code is stored hashed since 2.11.0) and purge_for_2_11_0()
478 + * does what dbDelta cannot: it empties the trusted devices, which were
479 + * identified by User-Agent until now (S1), and the pending email codes,
480 + * stored in clear until now (S11). Every remembered device asks for the
481 + * second factor once more after this update, and the changelog says so.
482 + */
483 + if ( version_compare( $db_version, '2.11.0', '<' ) ) {
484 + $this->database->create_tables();
485 + $this->database->purge_for_2_11_0();
486 +
487 + update_option( 'vigilante_db_version', '2.11.0' );
488 + }
489 +
490 + /*
491 + * 2.11.9: clear the raw .htaccess copies that older versions left in
492 + * options, on the first admin load after the update. Uninstall already
493 + * removes them, but that only fires when the plugin is deleted, so a
494 + * site that keeps the plugin carried them until now. Three stores, each
495 + * a copy of a file that can hold secrets (a SetEnv token, an
496 + * Authorization header): the same exposure the wp.org review flagged as
497 + * 4.4, on the paths its fix did not reach.
498 + *
499 + * - vigilante_htaccess_history: up to five raw copies, by design, until
500 + * 2.11.8. The writer is gone, nothing reads it, so it is deleted.
501 + * - vigilante_htaccess_backup: the single rollback buffer, normally
502 + * cleared in the finally of each write; a copy only lingers if a write
503 + * crashed mid-operation. Nothing outside one write reads it, so a
504 + * leftover is deleted.
505 + * - vigilante_htaccess_pre_migration: still read by the header recovery,
506 + * but older versions stored the whole file where only our own block is
507 + * ever used. Truncated to that block, so the feature keeps working and
508 + * nothing outside our markers stays in the option.
509 + */
510 + if ( version_compare( $db_version, '2.11.9', '<' ) ) {
511 + delete_option( 'vigilante_htaccess_history' );
512 + delete_option( 'vigilante_htaccess_backup' );
513 +
514 + $snapshot = get_option( 'vigilante_htaccess_pre_migration' );
515 + if ( is_array( $snapshot ) && isset( $snapshot['content'] ) && '' !== (string) $snapshot['content'] ) {
516 + require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-recovery.php';
517 + $block = Vigilante_Htaccess_Recovery::get_raw_block();
518 +
519 + if ( '' === $block ) {
520 + delete_option( 'vigilante_htaccess_pre_migration' );
521 + } elseif ( $block !== $snapshot['content'] ) {
522 + $snapshot['content'] = $block;
523 + update_option( 'vigilante_htaccess_pre_migration', $snapshot, false );
524 + }
525 + }
526 +
527 + update_option( 'vigilante_db_version', '2.11.9' );
528 + }
529 +
530 + /*
531 + * 2.11.10: the pending-approval flag becomes one per site on a network.
532 + * Until 2.11.9 it was a single global user meta, so the queue was shared
533 + * across the whole network. Moving the key is not enough: the accounts
534 + * already waiting carry the old key, and reading only the new one would
535 + * let them log in. So they are moved here, each to the site it belongs
536 + * to, and the old key is removed only once the new one is written.
537 + */
538 + if ( version_compare( $db_version, '2.11.10', '<' ) ) {
539 + $this->migrate_pending_approval_per_site();
540 +
541 + update_option( 'vigilante_db_version', '2.11.10' );
542 + }
543 +
544 + /*
545 + * 3.0.0: self-protection package. This block must stay the LAST one of
546 + * run_migrations(): every block compares against the same $db_version
547 + * captured at the top, so the last update_option() that runs is the
548 + * one that sticks, and on a jump from any older version it has to be
549 + * this one.
550 + *
551 + * 1. Capture the self-integrity anchor (the manifest fingerprint) and
552 + * run an inline self-check. It covers every update the old code never
553 + * saw through the upgrader hook: from any 2.x, and manual or FTP
554 + * uploads. A plain new is enough, the constructor registers no hooks.
555 + * 2. Drop the cached Security Check report: the Internal category grows
556 + * from 30 to 33 points (the self_integrity check), and the cached
557 + * report would keep the old denominator until the next full scan.
558 + * Same reason and same background refresh as the 2.6.1 block above.
559 + * 3. Remove Vigilant's own files from the ignore list before that check.
560 + * Until 2.11.11 the generic scan listed them like any plugin, and any
561 + * administrator could ignore one (after a false alarm while
562 + * WordPress.org published new checksums, for example). Kept, those
563 + * entries would silence the self-check of those files for good,
564 + * PHP included.
565 + */
566 + if ( version_compare( $db_version, '3.0.0', '<' ) ) {
567 + if ( ! class_exists( 'Vigilante_Self_Integrity' ) ) {
568 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity.php';
569 + }
570 + $ignored_files = get_option( 'vigilante_ignored_files', array() );
571 + if ( is_array( $ignored_files ) && $ignored_files ) {
572 + $kept_files = array_values(
573 + array_filter(
574 + $ignored_files,
575 + function ( $ignored_file ) {
576 + return ! Vigilante_Self_Integrity::is_own_file_path( (string) $ignored_file );
577 + }
578 + )
579 + );
580 + if ( count( $kept_files ) !== count( $ignored_files ) ) {
581 + update_option( 'vigilante_ignored_files', $kept_files );
582 + if ( $this->activity_log ) {
583 + $this->activity_log->log(
584 + 'system',
585 + 'self_ignored_files_removed',
586 + __( 'Vigilant files were removed from the File Integrity ignore list: self-protection checks them now.', 'vigilante' ),
587 + array( 'removed' => count( $ignored_files ) - count( $kept_files ) ),
588 + 'info'
589 + );
590 + }
591 + }
592 + }
593 + $self_integrity = new Vigilante_Self_Integrity( $this->settings, $this->activity_log );
594 + $self_integrity->run_check( 'migration' );
595 +
596 + delete_option( 'vigilante_analyzer_last_scan' );
597 + if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) {
598 + wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' );
599 + }
600 +
601 + update_option( 'vigilante_db_version', '3.0.0' );
602 + }
347 603 }
348 604
349 605 /**
606 + * Move the pending-approval flag of a network to a key per site
607 + *
608 + * Runs once for the whole network, not once per site: the data it moves is
609 + * global, so the guard is a network option and any site may be the one that
610 + * does it. On a single site the key does not change and there is nothing to
611 + * do.
612 + *
613 + * Each waiting account goes to its primary site, or to the only site it
614 + * belongs to; one that belongs to none goes to the main site rather than
615 + * nowhere, because losing the flag would silently approve it.
616 + *
617 + * @since 2.11.10
618 + */
619 + private function migrate_pending_approval_per_site() {
620 + global $wpdb;
621 +
622 + if ( ! is_multisite() ) {
623 + return;
624 + }
625 +
626 + if ( get_site_option( 'vigilante_pending_per_site_done' ) ) {
627 + return;
628 + }
629 +
630 + // 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".
631 + $user_ids = $wpdb->get_col(
632 + $wpdb->prepare( "SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s", 'vigilante_pending_approval' )
633 + );
634 +
635 + foreach ( (array) $user_ids as $user_id ) {
636 + $user_id = (int) $user_id;
637 + if ( ! $user_id ) {
638 + continue;
639 + }
640 +
641 + $pending = get_user_meta( $user_id, 'vigilante_pending_approval', true );
642 + $since = get_user_meta( $user_id, 'vigilante_pending_since', true );
643 +
644 + /*
645 + * Every site the account belongs to, not its primary one. The global
646 + * flag does not say where the registration happened, and the first
647 + * version of this guessed the primary blog: an account that
648 + * registered on B while its primary was A came out pending on A and
649 + * free to log in on B, which is the very site it had never been
650 + * approved on. Found by the cross review of 2.11.10.
651 + *
652 + * Marking every site it belongs to fails closed instead: the account
653 + * stays blocked wherever it can log in, and shows up in the queue of
654 + * each of those sites so somebody can actually act on it. An account
655 + * that belongs to no site goes to the main one rather than nowhere,
656 + * because losing the flag would silently approve it.
657 + */
658 + /*
659 + * With $all true, because the default leaves out archived, spam and
660 + * deleted sites (wp-includes/user.php:1113-1117): a site archived on
661 + * the day this runs would lose the flag, and the account would walk
662 + * in unapproved the moment it was brought back. Found by the second
663 + * cross review of 2.11.10.
664 + */
665 + $blog_ids = array();
666 +
667 + foreach ( get_blogs_of_user( $user_id, true ) as $blog ) {
668 + if ( ! empty( $blog->userblog_id ) ) {
669 + $blog_ids[] = (int) $blog->userblog_id;
670 + }
671 + }
672 +
673 + if ( empty( $blog_ids ) ) {
674 + $blog_ids[] = (int) get_main_site_id();
675 + }
676 +
677 + foreach ( array_unique( $blog_ids ) as $blog_id ) {
678 + $prefix = $wpdb->get_blog_prefix( $blog_id );
679 +
680 + update_user_meta( $user_id, $prefix . 'vigilante_pending_approval', $pending );
681 + if ( '' !== $since && false !== $since ) {
682 + update_user_meta( $user_id, $prefix . 'vigilante_pending_since', $since );
683 + }
684 + }
685 +
686 + delete_user_meta( $user_id, 'vigilante_pending_approval' );
687 + delete_user_meta( $user_id, 'vigilante_pending_since' );
688 + }
689 +
690 + update_site_option( 'vigilante_pending_per_site_done', 1 );
691 + }
692 +
693 + /**
350 694 * Migration: Remove orphaned email fields from saved options
351 695 *
352 696 * v1.10.0 centralized notification recipients into email section.
353 697 * Old per-module notify_email fields and dead email section fields
@@ -443,17 +787,30 @@
443 787 $pending_count = $this->get_pending_approvals_count();
444 788
445 789 // Get security issues with severity
446 790 $security_status = $this->get_security_status_for_badge();
447 -
791 +
792 + /*
793 + * Self-protection: a change to Vigilant own files is the one finding
794 + * that has to be visible from any screen of WordPress, so it adds to
795 + * the counter and paints it red. A verified state, a state with fewer
796 + * references than usual and the check turned off add nothing: a counter
797 + * that is always on is a counter nobody reads.
798 + */
799 + $self_tone = Vigilante_Self_Integrity::tone(
800 + Vigilante_Self_Integrity::display_state(),
801 + Vigilante_Self_Integrity::is_on()
802 + );
803 + $self_badge = in_array( $self_tone, array( 'critical', 'off', 'warning' ), true ) ? 1 : 0;
804 +
448 805 // Total count for badge
449 - $total_badge = $pending_count + $security_status['count'];
450 -
806 + $total_badge = $pending_count + $security_status['count'] + $self_badge;
807 +
451 808 if ( $total_badge > 0 ) {
452 809 // Determine badge color:
453 810 // - Red (awaiting-mod): pending approvals OR critical modules disabled
454 811 // - Orange (update-plugins): only non-critical modules disabled
455 - if ( $pending_count > 0 || $security_status['has_critical'] ) {
812 + if ( $pending_count > 0 || $security_status['has_critical'] || in_array( $self_tone, array( 'critical', 'off' ), true ) ) {
456 813 $badge_class = 'awaiting-mod';
457 814 } else {
458 815 $badge_class = 'update-plugins vigilante-badge-warning';
459 816 }
@@ -495,13 +852,21 @@
495 852 'vigilante-activity-log',
496 853 array( $this, 'redirect_to_tab' )
497 854 );
498 855
499 - // File Integrity shortcut
856 + // File Integrity shortcut, with its own counter when the self-check has
857 + // something to say: that is the screen that explains it.
858 + $fi_menu_title = __( 'File Integrity', 'vigilante' );
859 + if ( $self_badge > 0 ) {
860 + $fi_menu_title .= sprintf(
861 + ' <span class="%s count-1"><span class="pending-count">1</span></span>',
862 + in_array( $self_tone, array( 'critical', 'off' ), true ) ? 'awaiting-mod' : 'update-plugins vigilante-badge-warning'
863 + );
864 + }
500 865 add_submenu_page(
501 866 'vigilante',
502 867 __( 'File Integrity', 'vigilante' ),
503 - __( 'File Integrity', 'vigilante' ),
868 + $fi_menu_title,
504 869 'manage_options',
505 870 'vigilante-file-integrity',
506 871 array( $this, 'redirect_to_tab' )
507 872 );
@@ -615,23 +980,32 @@
615 980 if ( ! did_action( 'plugins_loaded' ) ) {
616 981 return 0;
617 982 }
618 983
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 - }
984 + /*
985 + * Counted whether the feature is on or off. An account already waiting
986 + * stays blocked when it is switched off (see init_enforcement_hooks()),
987 + * so reporting zero there hid people who cannot log in and whom nobody
988 + * could see to approve. Found by the cross review of 2.11.10.
989 + */
625 990
626 991 // 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',
992 + $args = array(
993 + 'meta_key' => Vigilante_User_Security::site_user_meta_key( 'vigilante_pending_approval' ),
629 994 'meta_value' => '1',
630 995 'fields' => 'ID',
631 - ) );
996 + );
632 997 // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
633 998
999 + // Same query as Vigilante_User_Security::get_pending_users(), and for the
1000 + // same reason: the meta key already scopes this to the site, and adding
1001 + // core's membership filter on top hid the accounts that have no role yet.
1002 + if ( is_multisite() ) {
1003 + $args['blog_id'] = 0;
1004 + }
1005 +
1006 + $pending_users = get_users( $args );
1007 +
634 1008 return count( $pending_users );
635 1009 }
636 1010
637 1011 /**
@@ -755,8 +1129,28 @@
755 1129 $score += 3;
756 1130 }
757 1131 }
758 1132
1133 + /*
1134 + * Self-protection (15 points). Having it on is configuration, which is
1135 + * what this card measures; the state of the last check caps the card,
1136 + * because a plugin whose own files were changed is not well configured
1137 + * in any useful sense, whatever the rest of the settings say. The
1138 + * environment block below already mixes measured state into this score
1139 + * (WP_DEBUG, insecure usernames), so the card keeps its meaning.
1140 + */
1141 + $max_score += 15;
1142 + $self_enabled = Vigilante_Self_Integrity::is_on();
1143 + $self_tone = Vigilante_Self_Integrity::tone( Vigilante_Self_Integrity::display_state(), $self_enabled );
1144 + if ( $self_enabled ) {
1145 + $score += 10;
1146 + // The five points are for a check that ran and came out clean: a
1147 + // site that has never checked itself has not earned them.
1148 + if ( in_array( $self_tone, array( 'ok', 'info' ), true ) ) {
1149 + $score += 5;
1150 + }
1151 + }
1152 +
759 1153 // Environment checks (8 points) - penalize insecure server configuration
760 1154 $max_score += 8;
761 1155 $env_score = 8;
762 1156
@@ -772,9 +1166,14 @@
772 1166 }
773 1167
774 1168 $score += max( 0, $env_score );
775 1169
776 - return $max_score > 0 ? round( ( $score / $max_score ) * 100 ) : 0;
1170 + $percent = $max_score > 0 ? (int) round( ( $score / $max_score ) * 100 ) : 0;
1171 + if ( in_array( $self_tone, array( 'critical', 'off' ), true ) ) {
1172 + // Same cap and same reason as the Security Check score.
1173 + $percent = min( $percent, Vigilante_Security_Analyzer::SCORE_CAP_ON_TAMPER );
1174 + }
1175 + return $percent;
777 1176 }
778 1177
779 1178 /**
780 1179 * Get security recommendations based on current settings
@@ -784,8 +1183,51 @@
784 1183 */
785 1184 private function get_security_recommendations( $options ) {
786 1185 $recommendations = array();
787 1186
1187 + // Self-protection first: if Vigilant itself cannot be trusted, nothing
1188 + // else on this card means much.
1189 + $vg_self_enabled = Vigilante_Self_Integrity::is_on();
1190 + $vg_self_tone = Vigilante_Self_Integrity::tone( Vigilante_Self_Integrity::display_state(), $vg_self_enabled );
1191 + if ( ! $vg_self_enabled ) {
1192 + $recommendations[] = array(
1193 + 'icon' => 'shield',
1194 + 'priority' => 'high',
1195 + 'tab' => 'file-integrity',
1196 + 'message' => __( 'Turn Vigilant self-protection on, so a change to Vigilant own files does not go unnoticed.', 'vigilante' ),
1197 + );
1198 + } elseif ( 'off' === $vg_self_tone ) {
1199 + $recommendations[] = array(
1200 + 'icon' => 'warning',
1201 + 'priority' => 'critical',
1202 + 'tab' => 'file-integrity',
1203 + 'message' => __( 'Something on this site switched Vigilant self-protection off: File Integrity says which file does it.', 'vigilante' ),
1204 + );
1205 + } elseif ( 'critical' === $vg_self_tone ) {
1206 + $recommendations[] = array(
1207 + 'icon' => 'warning',
1208 + 'priority' => 'critical',
1209 + 'tab' => 'file-integrity',
1210 + 'message' => __( 'Vigilant own files have been changed: repair Vigilant from File Integrity before anything else.', 'vigilante' ),
1211 + );
1212 + } elseif ( 'warning' === $vg_self_tone ) {
1213 + $recommendations[] = array(
1214 + 'icon' => 'shield',
1215 + 'priority' => 'high',
1216 + 'tab' => 'file-integrity',
1217 + 'message' => __( 'Vigilant self-protection needs your attention: File Integrity says what it found and what to do.', 'vigilante' ),
1218 + );
1219 + } elseif ( 'none' === $vg_self_tone ) {
1220 + // The score holds back the points of a check that has not run yet,
1221 + // so the card has to say why instead of just showing a lower number.
1222 + $recommendations[] = array(
1223 + 'icon' => 'shield',
1224 + 'priority' => 'high',
1225 + 'tab' => 'file-integrity',
1226 + 'message' => __( 'Vigilant has not checked its own files yet: run a scan from File Integrity.', 'vigilante' ),
1227 + );
1228 + }
1229 +
788 1230 // Critical: Firewall disabled
789 1231 if ( empty( $options['modules']['firewall'] ) ) {
790 1232 $recommendations[] = array(
791 1233 'icon' => 'warning',
@@ -1104,96 +1546,216 @@
1104 1546 );
1105 1547 }
1106 1548
1107 1549 /**
1108 - * Build the settings search index
1550 + * Index behind the settings search box.
1109 1551 *
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.
1552 + * Each entry points at one settings row. The search matches on the label,
1553 + * on its English original and on 'keywords', which are extra terms someone
1554 + * might type instead of the label itself.
1113 1555 *
1556 + * Those keywords are wrapped in _x() with the context "settings search
1557 + * keywords" so every locale can supply its own: the source strings are in
1558 + * English, and a Spanish user typing "contrasena" or a German one typing
1559 + * "Kennwort" only reaches the password settings if that locale translated
1560 + * them. Translators can add, drop or replace terms freely, one per space;
1561 + * they are never displayed, only matched against what the user types.
1562 + *
1563 + * The list is maintained by hand, so a new settings row needs an entry here
1564 + * or it cannot be found. It had drifted to 68 of 131 rows before 2.9.7.
1565 + *
1114 1566 * @return array
1115 1567 */
1116 1568 private function get_search_index() {
1117 1569 return array(
1118 1570 // 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' ),
1571 + 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' ) ),
1572 + 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' ) ),
1573 + 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' ) ),
1574 + 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' ) ),
1575 + 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' ) ),
1576 + 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' ) ),
1577 + 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' ) ),
1578 + 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 1579 // 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' ),
1580 + 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' ) ),
1581 + 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' ) ),
1582 + 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' ) ),
1583 + 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' ) ),
1584 + 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' ) ),
1585 + 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' ) ),
1586 + 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 1587 // 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' ),
1588 + 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' ) ),
1589 + 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' ) ),
1590 + 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' ) ),
1591 + 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' ) ),
1592 + 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' ) ),
1593 + 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' ) ),
1594 + 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' ) ),
1595 + // Security Headers - Cross-Origin Policies
1596 + 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' ) ),
1597 + 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' ) ),
1598 + 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 1599 // 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' ),
1600 + 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' ) ),
1601 + 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' ) ),
1602 + 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' ) ),
1603 + 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' ) ),
1604 + 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 1605 // 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' ),
1606 + 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' ) ),
1607 + 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' ) ),
1608 + 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 1609 // 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' ),
1610 + 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' ) ),
1611 + 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' ) ),
1612 + 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' ) ),
1613 + 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' ) ),
1614 + 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' ) ),
1615 + 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' ) ),
1616 + 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 1617 // 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' ),
1618 + 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' ) ),
1619 + 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' ) ),
1620 + 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' ) ),
1621 + 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' ) ),
1622 + 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' ) ),
1623 + 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' ) ),
1624 + 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' ) ),
1625 + 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' ) ),
1626 + 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' ) ),
1627 + 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' ) ),
1628 + 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' ) ),
1629 + 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 1630 // 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' ),
1631 + 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' ) ),
1632 + 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' ) ),
1633 + 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' ) ),
1634 + array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'Vigilant self-protection', 'vigilante' ), 'anchor' => 'vigilante-section-fi-self', 'label' => __( 'Vigilant self-protection', 'vigilante' ), 'label_en' => 'Vigilant self-protection', 'keywords' => _x( 'self protection selfprotection self-check autoproteccion manifest sha256 checksums tampering tampered repair reinstall own files guardian integrity of the plugin', 'settings search keywords', 'vigilante' ) ),
1635 + 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 1636 // 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' ),
1637 + 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' ) ),
1638 + 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' ) ),
1639 + 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' ) ),
1640 + 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' ) ),
1641 + 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' ) ),
1642 + 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' ) ),
1643 + 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' ) ),
1644 + 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 1645 // 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' ),
1646 + 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' ) ),
1647 + 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' ) ),
1648 + 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' ) ),
1649 + 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' ) ),
1650 + 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' ) ),
1651 + 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' ) ),
1652 + 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' ) ),
1653 + // Entradas anadidas en la 2.9.7 tras comprobar que el indice cubria 68 de
1654 + // las 131 filas de ajustes: buscar XML-RPC, por ejemplo, no devolvia nada.
1655 + // El indice se mantiene a mano, asi que al anadir una fila de ajustes hay
1656 + // que anadirla tambien aqui.
1657 + 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' ) ),
1658 + 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' ) ),
1659 + 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' ) ),
1660 + 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' ) ),
1661 + 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' ) ),
1662 + 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' ) ),
1663 + 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' ) ),
1664 + 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' ) ),
1665 + 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' ) ),
1666 + 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' ) ),
1667 + 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' ) ),
1668 + 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' ) ),
1669 + 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' ) ),
1670 + 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' ) ),
1671 + 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' ) ),
1672 + 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' ) ),
1673 + 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' ) ),
1674 + 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' ) ),
1675 + 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' ) ),
1676 + 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' ) ),
1677 + 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' ) ),
1678 + 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' ) ),
1679 + 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' ) ),
1680 + 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' ) ),
1681 + 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' ) ),
1682 + 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' ) ),
1683 + 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' ) ),
1684 + 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' ) ),
1685 + 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' ) ),
1686 + 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' ) ),
1687 + 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' ) ),
1688 + 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' ) ),
1689 + 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' ) ),
1690 + 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' ) ),
1691 + 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' ) ),
1692 + 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' ) ),
1693 + 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' ) ),
1694 + 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' ) ),
1695 + 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' ) ),
1696 + 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' ) ),
1697 + 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' ) ),
1698 + 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' ) ),
1699 + 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' ) ),
1700 + 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' ) ),
1701 + 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' ) ),
1702 + 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' ) ),
1703 + 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' ) ),
1704 + 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' ) ),
1705 + 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' ) ),
1706 + 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' ) ),
1707 + 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' ) ),
1708 + 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' ) ),
1709 + 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' ) ),
1710 + 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' ) ),
1711 + 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' ) ),
1712 + 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' ) ),
1713 + 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' ) ),
1714 + 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' ) ),
1715 + 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' ) ),
1716 + 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' ) ),
1717 + 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' ) ),
1718 + 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' ) ),
1719 + 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' ) ),
1720 + 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' ) ),
1721 + 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' ) ),
1722 + 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' ) ),
1723 + 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' ) ),
1724 + 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' ) ),
1725 + 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' ) ),
1726 + 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' ) ),
1727 + 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' ) ),
1728 + 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' ) ),
1729 + 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' ) ),
1730 + 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' ) ),
1731 + 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' ) ),
1732 + 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' ) ),
1733 + 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' ) ),
1734 + 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' ) ),
1735 + 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' ) ),
1736 + 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' ) ),
1737 + 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' ) ),
1738 + 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' ) ),
1739 + 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' ) ),
1740 + 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' ) ),
1741 + 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' ) ),
1742 + 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' ) ),
1743 + 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' ) ),
1744 + 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' ) ),
1745 + 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' ) ),
1746 + 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' ) ),
1747 + 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' ) ),
1748 + 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' ) ),
1749 + 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' ) ),
1750 + 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' ) ),
1751 + 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' ) ),
1752 + 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' ) ),
1753 + 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' ) ),
1754 + 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' ) ),
1755 + 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' ) ),
1756 + 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' ) ),
1757 + 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 1758 );
1197 1759 }
1198 1760
1199 1761 /**
@@ -1223,13 +1785,17 @@
1223 1785 );
1224 1786
1225 1787 wp_localize_script( 'vigilante-admin', 'vigilanteAdmin', array(
1226 1788 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
1789 + 'selfBoxUrl' => esc_url( admin_url( 'admin.php?page=vigilante&tab=file-integrity#vigilante-section-fi-self' ) ),
1227 1790 'nonce' => wp_create_nonce( 'vigilante_admin_nonce' ),
1228 1791 'currentUserId' => get_current_user_id(),
1229 1792 'logoutUrl' => wp_logout_url( wp_login_url() ),
1230 1793 'adminUrl' => admin_url( 'admin.php?page=vigilante' ),
1231 1794 'searchIndex' => $this->get_search_index(),
1795 + // The scan repaints this table from JavaScript, so the same gate
1796 + // has to travel with it or half the screen keeps the dead button.
1797 + 'approvalLocked' => $this->critical_approval_locked(),
1232 1798 'underAttack' => array(
1233 1799 'active' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->is_active(),
1234 1800 'remaining' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->get_remaining_time(),
1235 1801 ),
@@ -1269,8 +1835,13 @@
1269 1835 'file' => __( 'File', 'vigilante' ),
1270 1836 'reason' => __( 'Reason', 'vigilante' ),
1271 1837 'type' => __( 'Type', 'vigilante' ),
1272 1838 'unknown' => __( 'Unknown', 'vigilante' ),
1839 + 'selfType' => __( 'Vigilant (self)', 'vigilante' ),
1840 + 'logWhatHappened' => __( 'What happened', 'vigilante' ),
1841 + 'logWhatItMeans' => __( 'What it means', 'vigilante' ),
1842 + 'logWhatToDo' => __( 'What to do', 'vigilante' ),
1843 + 'logSelfSeeDetails' => __( 'Open File Integrity for the full detail', 'vigilante' ),
1273 1844 'modifiedFiles' => __( 'Modified Files', 'vigilante' ),
1274 1845 'modifiedDescription' => __( 'These files (apparently) differ from the original WordPress or plugin versions.', 'vigilante' ),
1275 1846 'extraFiles' => __( 'Extra Files', 'vigilante' ),
1276 1847 'extra' => __( 'Extra', 'vigilante' ),
@@ -1296,13 +1867,17 @@
1296 1867 'criticalConfigTitle' => __( 'Critical config files modified', 'vigilante' ),
1297 1868 '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 1869 'approve' => __( 'Approve', 'vigilante' ),
1299 1870 'approving' => __( 'Approving...', 'vigilante' ),
1871 + 'approvalLockedNotice' => $this->critical_approval_notice(),
1300 1872 'criticalApproved' => __( 'Change approved. Next scan will use the current state as baseline.', 'vigilante' ),
1301 1873 'reviewChanges' => __( 'Review changes', 'vigilante' ),
1302 1874 'hideChanges' => __( 'Hide changes', 'vigilante' ),
1303 1875 'changes' => __( 'Changes', 'vigilante' ),
1304 1876 'diffUnavailable' => __( 'Diff not available for this file (baseline was created before diff tracking was added). Approve to enable diff on future changes.', 'vigilante' ),
1877 + 'diffNetwork' => __( 'This file belongs to the whole network, so its line changes are only shown to network administrators, on the main site.', 'vigilante' ),
1878 + 'diffRescan' => __( 'Run a new scan to see the line changes of this file.', 'vigilante' ),
1879 + '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 1880 'diffEmpty' => __( 'No line-level changes detected (may be whitespace or reordering).', 'vigilante' ),
1306 1881 'diffLines' => __( 'lines', 'vigilante' ),
1307 1882 // Under Attack mode strings
1308 1883 'underAttackConfirmActivate' => __( 'Activate Under Attack mode? All visitors will see a verification page for the next 4 hours.', 'vigilante' ),
@@ -1358,8 +1933,9 @@
1358 1933 'logType' => __( 'Type', 'vigilante' ),
1359 1934 'logAction' => __( 'Action', 'vigilante' ),
1360 1935 'logSeverity' => __( 'Severity', 'vigilante' ),
1361 1936 'logMessage' => __( 'Message', 'vigilante' ),
1937 + 'logRequestUri' => __( 'Address', 'vigilante' ),
1362 1938 'logClient' => __( 'Client', 'vigilante' ),
1363 1939 'logUser' => __( 'User', 'vigilante' ),
1364 1940 'logIpAddress' => __( 'IP Address', 'vigilante' ),
1365 1941 'logUserAgent' => __( 'User Agent', 'vigilante' ),
@@ -1394,8 +1970,10 @@
1394 1970 /* translators: 1: selected count, 2: human-readable size */
1395 1971 'dbTablesSelected' => __( '%1$d tables selected (%2$s)', 'vigilante' ),
1396 1972 // Settings search strings
1397 1973 'searchNoResults' => __( 'No matching settings found.', 'vigilante' ),
1974 + /* translators: %d: number of results that did not fit in the list. */
1975 + 'searchMoreResults' => __( '%d more results. Refine the search to see them.', 'vigilante' ),
1398 1976 'searchInTab' => __( 'in', 'vigilante' ),
1399 1977 // Modules string
1400 1978 /* translators: 1: enabled count, 2: total count */
1401 1979 'modulesEnabled' => __( '%1$d / %2$d modules enabled', 'vigilante' ),
@@ -1515,9 +2093,89 @@
1515 2093
1516 2094 /**
1517 2095 * Show admin notices
1518 2096 */
2097 + /**
2098 + * Self-protection notice.
2099 + *
2100 + * A change to Vigilant own files is shown on every admin screen, because
2101 + * waiting for someone to open the plugin is exactly what an attacker who
2102 + * patched it would want. A warning is shown only on Vigilant screens, so
2103 + * the notice that matters is not diluted. Neither is dismissible, both are
2104 + * for administrators only, and on a network the steps depend on whether
2105 + * the person can install plugins at all.
2106 + */
2107 + public function maybe_show_self_protection_notice() {
2108 + if ( ! current_user_can( 'manage_options' ) ) {
2109 + return;
2110 + }
2111 + $summary = $this->self_integrity_summary();
2112 + $tone = $summary['tone'];
2113 + if ( ! in_array( $tone, array( 'critical', 'off', 'warning' ), true ) ) {
2114 + return;
2115 + }
2116 +
2117 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading the screen slug to decide where a notice is shown; no action taken.
2118 + $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
2119 + $on_vigilante = ( 0 === strpos( $page, 'vigilante' ) );
2120 + if ( 'warning' === $tone && ! $on_vigilante ) {
2121 + return;
2122 + }
2123 + $alarma = in_array( $tone, array( 'critical', 'off' ), true );
2124 +
2125 + $link = admin_url( 'admin.php?page=vigilante&tab=file-integrity#vigilante-section-fi-self' );
2126 + $network = is_multisite() && ! current_user_can( 'update_plugins' );
2127 + ?>
2128 + <div class="notice notice-<?php echo $alarma ? 'error' : 'warning'; ?> vigilante-self-notice">
2129 + <p class="vigilante-self-notice-title">
2130 + <span class="dashicons dashicons-shield" aria-hidden="true"></span>
2131 + <strong>
2132 + <?php
2133 + if ( 'critical' === $tone ) {
2134 + esc_html_e( 'Vigilant detected changes in its own files', 'vigilante' );
2135 + } elseif ( 'off' === $tone ) {
2136 + esc_html_e( 'Vigilant self-protection is switched off by code', 'vigilante' );
2137 + } else {
2138 + echo esc_html( $this->self_integrity_headline( $tone, $summary['state'] ) );
2139 + }
2140 + ?>
2141 + </strong>
2142 + </p>
2143 + <p class="vigilante-self-notice-text">
2144 + <span>
2145 + <?php
2146 + if ( 'critical' === $tone ) {
2147 + esc_html_e( 'Your security plugin may have been tampered with, and while that is true nothing it reports can be trusted.', 'vigilante' );
2148 + } elseif ( 'off' === $tone ) {
2149 + esc_html_e( 'Nothing is checking that Vigilant own files are intact. File Integrity says which file switches it off.', 'vigilante' );
2150 + } else {
2151 + esc_html_e( 'File Integrity says what was found, what it means and what to do about it.', 'vigilante' );
2152 + }
2153 + if ( $network ) {
2154 + echo ' ' . esc_html__( 'Only your network administrator can repair Vigilant, because its files are shared by every site in the network. Let them know.', 'vigilante' );
2155 + }
2156 + ?>
2157 + </span>
2158 + <?php
2159 + /*
2160 + * One button, on the same line as the text it belongs to, and it
2161 + * leads to the whole story. Repairing from a notice, without
2162 + * seeing which files, what it means and what it will do, is
2163 + * asking someone to fix what they have not read: the repair
2164 + * button lives in the card, after all that.
2165 + */
2166 + ?>
2167 + <a class="button button-primary button-small" href="<?php echo esc_url( $link ); ?>">
2168 + <?php esc_html_e( 'See what to do', 'vigilante' ); ?>
2169 + </a>
2170 + </p>
2171 + </div>
2172 + <?php
2173 + }
2174 +
1519 2175 public function show_admin_notices() {
2176 + $this->maybe_show_self_protection_notice();
2177 +
1520 2178 // Activation notice
1521 2179 if ( get_transient( 'vigilante_activated' ) ) {
1522 2180 ?>
1523 2181 <div class="notice notice-success is-dismissible vigilante-activation-notice">
@@ -1562,8 +2220,21 @@
1562 2220 </p>
1563 2221 <p>
1564 2222 <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 2223 </p>
2224 + <?php
2225 + // The cache-bypass rules could not be written (a host where
2226 + // WordPress cannot write files by itself, a held lock, a
2227 + // failed read-back): show them, so they can be added by hand.
2228 + $ua_instance = new Vigilante_Under_Attack( $this->settings, $this->activity_log );
2229 + if ( $ua_instance->cache_rules_missing() ) :
2230 + ?>
2231 + <p>
2232 + <strong><?php esc_html_e( 'The cache-bypass rules could not be written to your .htaccess.', 'vigilante' ); ?></strong>
2233 + <?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' ); ?>
2234 + </p>
2235 + <textarea readonly rows="9" class="large-text code" onclick="this.select();"><?php echo esc_textarea( Vigilante_Under_Attack::get_cache_bypass_block() ); ?></textarea>
2236 + <?php endif; ?>
1566 2237 </div>
1567 2238 <?php
1568 2239 }
1569 2240 }
@@ -1665,9 +2336,9 @@
1665 2336 </h1>
1666 2337 <div class="vigilante-search-wrapper">
1667 2338 <div class="vigilante-search-input-wrap">
1668 2339 <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">
2340 + <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 2341 <span class="vigilante-search-shortcut" aria-hidden="true">/</span>
1671 2342 </div>
1672 2343 <div id="vigilante-settings-search-results" class="vigilante-search-results" hidden role="listbox"></div>
1673 2344 </div>
@@ -1749,8 +2420,204 @@
1749 2420 <?php
1750 2421 }
1751 2422
1752 2423 /**
2424 + * Values to display for a section that this site does not control
2425 + *
2426 + * On a subsite the stored options are its own copy, which nothing acts on:
2427 + * wp-config.php and .htaccess are written from the main site. Painting the
2428 + * local copy describes a configuration that is not running, so a subsite
2429 + * admin sees a box ticked here and the constant absent from the file, or the
2430 + * other way round. Read the main site's values instead, which are the ones in
2431 + * force, and fall back to the local ones if they cannot be read.
2432 + *
2433 + * @since 2.9.8
2434 + *
2435 + * @param string $section Settings section.
2436 + * @return array
2437 + */
2438 + private function get_section_for_display( $section ) {
2439 + $local = $this->settings->get_section( $section );
2440 +
2441 + if ( ! $this->shared_files_locked() ) {
2442 + return $local;
2443 + }
2444 +
2445 + // shared_files_locked() is only true on multisite, where get_blog_option() exists.
2446 + $main = get_blog_option( get_main_site_id(), Vigilante_Settings::OPTION_NAME, array() );
2447 +
2448 + if ( ! is_array( $main ) || empty( $main[ $section ] ) || ! is_array( $main[ $section ] ) ) {
2449 + return $local;
2450 + }
2451 +
2452 + return wp_parse_args( $main[ $section ], $local );
2453 + }
2454 +
2455 + /**
2456 + * Whether the sections that write wp-config.php and .htaccess are read-only here
2457 + *
2458 + * True on a network when this is not the main site, or the user is not a
2459 + * network administrator. See Vigilante_Settings::can_write_shared_files().
2460 + *
2461 + * @since 2.9.8
2462 + *
2463 + * @return bool
2464 + */
2465 + private function shared_files_locked() {
2466 + return ! Vigilante_Settings::can_write_shared_files();
2467 + }
2468 +
2469 + /**
2470 + * Whether this is the main site and the user cannot change what it builds the shared files from
2471 + *
2472 + * See Vigilante_Settings::get_main_site_file_settings(). On a subsite those
2473 + * settings only act on that site, so they are never locked there.
2474 + *
2475 + * @since 2.11.6
2476 + *
2477 + * @return bool
2478 + */
2479 + private function main_site_files_locked() {
2480 + return $this->shared_files_locked() && Vigilante_Settings::owns_shared_files();
2481 + }
2482 +
2483 + /**
2484 + * Sentence added to a bulk change when some settings were left as they were
2485 + *
2486 + * Importing a file, applying a preset and restoring the defaults touch every
2487 + * section at once, so the user is told that the shared file settings did
2488 + * not move.
2489 + *
2490 + * @since 2.11.6
2491 + *
2492 + * @return string Empty when the user can change every setting.
2493 + */
2494 + private function locked_file_settings_message() {
2495 + if ( ! Vigilante_Settings::get_locked_file_settings() ) {
2496 + return '';
2497 + }
2498 +
2499 + return ' ' . __( 'The settings that end up in wp-config.php or .htaccess were left as they were.', 'vigilante' ) . ' ' . Vigilante_Settings::get_shared_files_notice();
2500 + }
2501 +
2502 + /**
2503 + * Print the shared-files notice for a section that cannot be edited here
2504 + *
2505 + * @since 2.9.8
2506 + */
2507 + private function render_shared_files_notice() {
2508 + if ( ! $this->shared_files_locked() ) {
2509 + return;
2510 + }
2511 + ?>
2512 + <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;">
2513 + <p style="margin:0;"><?php echo esc_html( Vigilante_Settings::get_shared_files_notice() ); ?></p>
2514 + </div>
2515 + <?php
2516 + }
2517 +
2518 + /**
2519 + * Acting on another user's account needs permission over that user
2520 + *
2521 + * Since 2.10.3 the handlers behind these tools ask for edit_user over the
2522 + * target, which is the rule WordPress itself applies. On a network the core
2523 + * grants edit_user only to network administrators, so for anybody else these
2524 + * controls do nothing. Better to say so than to paint a button that silently
2525 + * skips every user.
2526 + *
2527 + * @since 2.10.4
2528 + * @return bool
2529 + */
2530 + private function forwarded_chain_readings() {
2531 + // Shown, not decided on: the firewall resolves the address elsewhere.
2532 + $chain = Vigilante_IP_Utils::trusted_forwarded_for();
2533 +
2534 + if ( '' === $chain ) {
2535 + return array();
2536 + }
2537 +
2538 + $public = array();
2539 +
2540 + foreach ( explode( ',', $chain ) as $entry ) {
2541 + $address = Vigilante_IP_Utils::unmap_ipv4( trim( $entry ) );
2542 +
2543 + if ( filter_var( $address, FILTER_VALIDATE_IP ) && ! Vigilante_IP_Utils::is_own_network( $address ) ) {
2544 + $public[] = $address;
2545 + }
2546 + }
2547 +
2548 + if ( count( $public ) < 2 ) {
2549 + return array();
2550 + }
2551 +
2552 + return array(
2553 + 'now' => Vigilante_IP_Utils::client_from_chain( $chain ),
2554 + 'before' => $public[0],
2555 + );
2556 + }
2557 +
2558 + /**
2559 + * Whether the user tools of this screen are out of reach for this user
2560 + *
2561 + * @return bool
2562 + */
2563 + private function user_actions_locked() {
2564 + // On a single site edit_user maps to edit_users, which a custom role with
2565 + // manage_options may lack: since 2.11.8 approving and rejecting a pending
2566 + // registration ask for it, so the buttons have to say so there too.
2567 + return is_multisite() ? ! current_user_can( 'manage_network_users' ) : ! current_user_can( 'edit_users' );
2568 + }
2569 +
2570 + /**
2571 + * Print the notice for user tools that cannot be used from this site
2572 + *
2573 + * @since 2.10.4
2574 + */
2575 + private function render_user_actions_notice() {
2576 + if ( ! $this->user_actions_locked() ) {
2577 + return;
2578 + }
2579 + ?>
2580 + <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;">
2581 + <?php if ( is_multisite() ) : ?>
2582 + <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>
2583 + <?php else : ?>
2584 + <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>
2585 + <?php endif; ?>
2586 + </div>
2587 + <?php
2588 + }
2589 +
2590 + /**
2591 + * Approving a change to the shared config files needs the network
2592 + *
2593 + * Since 2.11.3 the handler behind the Approve button asks for
2594 + * manage_network_options, because the two files it approves, wp-config.php
2595 + * and the root .htaccess, belong to the installation, and so does the
2596 + * record of them. The button, though, went on being painted for everybody,
2597 + * so the administrator of a subsite saw the warning, saw the button,
2598 + * pressed it and got "Permission denied" with no explanation. That is
2599 + * exactly what user_actions_locked() above exists to avoid, one release
2600 + * later and one screen over. Flagged by @calzbert.
2601 + *
2602 + * @since 2.11.4
2603 + * @return bool
2604 + */
2605 + private function critical_approval_locked() {
2606 + return is_multisite() && ! current_user_can( 'manage_network_options' );
2607 + }
2608 +
2609 + /**
2610 + * The line that replaces the Approve button where it cannot be used
2611 + *
2612 + * @since 2.11.4
2613 + * @return string
2614 + */
2615 + private function critical_approval_notice() {
2616 + 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' );
2617 + }
2618 +
2619 + /**
1753 2620 * Check if module is disabled and render warning
1754 2621 *
1755 2622 * @param string $module_key Module key.
1756 2623 * @return bool True if disabled.
@@ -1811,8 +2678,22 @@
1811 2678 $categories = isset( $last_scan['categories'] ) && is_array( $last_scan['categories'] ) ? $last_scan['categories'] : array();
1812 2679 $weekly_enabled = ! isset( $analyzer_settings['weekly_scan_enabled'] ) || ! empty( $analyzer_settings['weekly_scan_enabled'] );
1813 2680 $email_enabled = ! empty( $analyzer_settings['email_on_regression'] );
1814 2681
2682 + /*
2683 + * Self-protection caps the score, and it does so here and not only in
2684 + * the stored report: the report is reused until the next Security
2685 + * Check, so tampering found after it ran would otherwise be shown next
2686 + * to the score the site earned before it happened.
2687 + */
2688 + $self_summary = $this->self_integrity_summary();
2689 + $self_capped = in_array( $self_summary['tone'], array( 'critical', 'off' ), true )
2690 + || ( 'self_integrity' === ( isset( $last_scan['capped_by'] ) ? $last_scan['capped_by'] : '' ) );
2691 + if ( $self_capped && $has_data && $score > Vigilante_Security_Analyzer::SCORE_CAP_ON_TAMPER ) {
2692 + $score = Vigilante_Security_Analyzer::SCORE_CAP_ON_TAMPER;
2693 + $grade = 'E';
2694 + }
2695 +
1815 2696 $quality = self::analyzer_quality_tag( $score );
1816 2697 ?>
1817 2698 <div class="vigilante-analyzer" id="vigilante-analyzer"
1818 2699 data-has-data="<?php echo $has_data ? '1' : '0'; ?>">
@@ -1837,8 +2718,19 @@
1837 2718 </button>
1838 2719 </div>
1839 2720 </div>
1840 2721
2722 + <?php if ( $self_capped ) : ?>
2723 + <p class="vigilante-analyzer-capped">
2724 + <span class="dashicons dashicons-shield" aria-hidden="true"></span>
2725 + <strong><?php esc_html_e( 'This score is not reliable right now.', 'vigilante' ); ?></strong>
2726 + <?php esc_html_e( 'Vigilant own files have been changed, and every other result on this page is produced by that same code. The score is held at the bottom of the scale until the files verify clean again.', 'vigilante' ); ?>
2727 + <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=file-integrity#vigilante-section-fi-self' ) ); ?>">
2728 + <?php esc_html_e( 'See what to do', 'vigilante' ); ?>
2729 + </a>
2730 + </p>
2731 + <?php endif; ?>
2732 +
1841 2733 <div class="vigilante-analyzer-summary">
1842 2734 <div class="vigilante-analyzer-score-card">
1843 2735 <?php if ( $has_data && $grade ) : ?>
1844 2736 <div class="vigilante-score-circle vigilante-grade-<?php echo esc_attr( strtolower( $grade ) ); ?>">
@@ -2212,8 +3104,58 @@
2212 3104
2213 3105 /**
2214 3106 * Render dashboard tab
2215 3107 */
3108 + /**
3109 + * One line strip at the top of the Dashboard tab: the state of Vigilant own
3110 + * files, above the Configuration Score and the Security Check, because
3111 + * nothing else on this screen means much while it is red.
3112 + */
3113 + private function render_self_protection_strip() {
3114 + $summary = $this->self_integrity_summary();
3115 + $tone = $summary['tone'];
3116 + $state = $summary['state'];
3117 + $count = count( $summary['findings'] );
3118 + $link = admin_url( 'admin.php?page=vigilante&tab=file-integrity#vigilante-section-fi-self' );
3119 + ?>
3120 + <div class="vigilante-self-strip vigilante-self-strip--<?php echo esc_attr( $tone ); ?>">
3121 + <span class="dashicons dashicons-shield" aria-hidden="true"></span>
3122 + <strong><?php esc_html_e( 'Vigilant self-protection', 'vigilante' ); ?></strong>
3123 + <span class="vigilante-self-strip-state"><?php echo esc_html( $this->self_integrity_headline( $tone, $state ) ); ?></span>
3124 + <?php if ( $count > 0 ) : ?>
3125 + <span class="vigilante-self-strip-count">
3126 + <?php
3127 + printf(
3128 + /* translators: %d: number of findings about Vigilant own files */
3129 + esc_html( _n( '%d finding', '%d findings', $count, 'vigilante' ) ),
3130 + (int) $count
3131 + );
3132 + ?>
3133 + </span>
3134 + <?php elseif ( ! empty( $state['last_check'] ) && 'off' !== $tone ) : ?>
3135 + <span class="vigilante-self-strip-count">
3136 + <?php
3137 + printf(
3138 + /* translators: %s: human time difference, like "2 hours" */
3139 + esc_html__( 'checked %s ago', 'vigilante' ),
3140 + esc_html( human_time_diff( (int) $state['last_check'], time() ) )
3141 + );
3142 + ?>
3143 + </span>
3144 + <?php endif; ?>
3145 + <a href="<?php echo esc_url( $link ); ?>">
3146 + <?php
3147 + if ( in_array( $tone, array( 'critical', 'off', 'warning' ), true ) ) {
3148 + esc_html_e( 'See what to do', 'vigilante' );
3149 + } else {
3150 + esc_html_e( 'See details', 'vigilante' );
3151 + }
3152 + ?>
3153 + </a>
3154 + </div>
3155 + <?php
3156 + }
3157 +
2216 3158 private function render_tab_dashboard() {
2217 3159 $options = $this->settings->get_all_options();
2218 3160 $module_labels = $this->settings->get_module_labels();
2219 3161 $module_descriptions = $this->settings->get_module_descriptions();
@@ -2233,8 +3175,9 @@
2233 3175 $analyzer_categories_def = Vigilante_Security_Analyzer::get_categories();
2234 3176 $analyzer_settings = isset( $options['security_analyzer'] ) ? $options['security_analyzer'] : array();
2235 3177 ?>
2236 3178 <div class="vigilante-dashboard">
3179 + <?php $this->render_self_protection_strip(); ?>
2237 3180 <div class="vigilante-status-card">
2238 3181 <h2><?php esc_html_e( 'Configuration Score', 'vigilante' ); ?></h2>
2239 3182 <p class="vigilante-score-kind description">
2240 3183 <?php esc_html_e( 'How well Vigilante is configured right now. Pair it with the Security Check below to see the real-world result.', 'vigilante' ); ?>
@@ -2294,14 +3237,15 @@
2294 3237
2295 3238 <?php $this->render_analyzer_widget( $analyzer_last_scan, $analyzer_history, $analyzer_categories_def, $analyzer_settings ); ?>
2296 3239
2297 3240 <div class="vigilante-modules-grid">
2298 - <h2><?php esc_html_e( 'Security Modules', 'vigilante' ); ?></h2>
3241 + <h2 id="vigilante-section-dashboard-modules"><?php esc_html_e( 'Security Modules', 'vigilante' ); ?></h2>
2299 3242 <p class="description"><?php esc_html_e( 'Enable or disable security modules. Each module controls a tab with detailed settings.', 'vigilante' ); ?></p>
2300 3243 <div class="vigilante-modules-list">
2301 3244 <?php foreach ( $options['modules'] as $module => $enabled ) :
2302 3245 $label = isset( $module_labels[ $module ] ) ? $module_labels[ $module ] : ucwords( str_replace( '_', ' ', $module ) );
2303 3246 $description = isset( $module_descriptions[ $module ] ) ? $module_descriptions[ $module ] : '';
3247 + $vg_module_locked = $this->main_site_files_locked() && in_array( $module, Vigilante_Settings::get_main_site_file_settings()['modules'], true );
2304 3248 ?>
2305 3249 <div class="vigilante-module-item <?php echo $enabled ? 'enabled' : 'disabled'; ?>">
2306 3250 <div class="vigilante-module-header">
2307 3251 <span class="vigilante-module-status"></span>
@@ -2314,8 +3258,9 @@
2314 3258 <input type="checkbox"
2315 3259 name="modules[<?php echo esc_attr( $module ); ?>]"
2316 3260 value="1"
2317 3261 <?php checked( $enabled ); ?>
3262 + <?php disabled( $vg_module_locked ); ?>
2318 3263 aria-label="<?php echo esc_attr( $toggle_label ); ?>"
2319 3264 data-module="<?php echo esc_attr( $module ); ?>">
2320 3265 <span class="vigilante-toggle-slider"></span>
2321 3266 </label>
@@ -2322,8 +3267,11 @@
2322 3267 </div>
2323 3268 <?php if ( $description ) : ?>
2324 3269 <p class="vigilante-module-desc"><?php echo esc_html( $description ); ?></p>
2325 3270 <?php endif; ?>
3271 + <?php if ( $vg_module_locked ) : ?>
3272 + <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>
3273 + <?php endif; ?>
2326 3274 </div>
2327 3275 <?php endforeach; ?>
2328 3276 </div>
2329 3277 </div>
@@ -2355,9 +3303,9 @@
2355 3303 $ua_remaining_hours = floor( $ua_remaining / 3600 );
2356 3304 $ua_remaining_mins = floor( ( $ua_remaining % 3600 ) / 60 );
2357 3305 ?>
2358 3306 <div class="vigilante-preset-card vigilante-under-attack-card <?php echo $ua_active ? 'vigilante-under-attack-active' : ''; ?>">
2359 - <h3>
3307 + <h3 id="vigilante-section-dashboard-under-attack">
2360 3308 <span class="dashicons dashicons-shield"></span>
2361 3309 <?php esc_html_e( 'Under Attack', 'vigilante' ); ?>
2362 3310 </h3>
2363 3311 <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 +3383,11 @@
2435 3383 </label>
2436 3384 </td>
2437 3385 </tr>
2438 3386 <tr>
2439 - <th scope="row"><?php esc_html_e( 'Additional Recipients', 'vigilante' ); ?></th>
3387 + <th scope="row"><label for="vigilante-f-email-additional-recipients"><?php esc_html_e( 'Additional Recipients', 'vigilante' ); ?></label></th>
2440 3388 <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>
3389 + <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 3390 <p class="description"><?php esc_html_e( 'One email per line.', 'vigilante' ); ?></p>
2443 3391 </td>
2444 3392 </tr>
2445 3393 <tr>
@@ -2614,9 +3562,9 @@
2614 3562
2615 3563 <div class="vigilante-tool-card">
2616 3564 <h3><?php esc_html_e( 'Import Settings', 'vigilante' ); ?></h3>
2617 3565 <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;">
3566 + <input type="file" id="vigilante-import-file" aria-label="<?php esc_attr_e( 'Configuration file to import', 'vigilante' ); ?>" accept=".json" style="display: none;">
2619 3567 <button type="button" class="button vigilante-import-settings">
2620 3568 <?php esc_html_e( 'Import Settings', 'vigilante' ); ?>
2621 3569 </button>
2622 3570 </div>
@@ -2622,9 +3570,9 @@
2622 3570 </div>
2623 3571
2624 3572 <div class="vigilante-tool-card">
2625 3573 <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>
3574 + <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 3575 <button type="button" class="button vigilante-reset-settings" style="color: #a00;">
2628 3576 <?php esc_html_e( 'Reset All Settings', 'vigilante' ); ?>
2629 3577 </button>
2630 3578 </div>
@@ -2631,13 +3579,19 @@
2631 3579
2632 3580 <div class="vigilante-tool-card">
2633 3581 <h3><?php esc_html_e( 'Download Config Backup', 'vigilante' ); ?></h3>
2634 3582 <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>
3583 + <?php if ( $this->shared_files_locked() ) : ?>
3584 + <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>
3585 + <?php else : ?>
2635 3586 <button type="button" class="button vigilante-create-backup">
2636 3587 <?php esc_html_e( 'Download Backup', 'vigilante' ); ?>
2637 3588 </button>
3589 + <?php endif; ?>
2638 3590 </div>
2639 3591
3592 + <?php if ( ! $this->shared_files_locked() ) : ?>
3593 +
2640 3594 <div class="vigilante-tool-card vigilante-tool-card-wide">
2641 3595 <h3><?php esc_html_e( 'Database Backup', 'vigilante' ); ?></h3>
2642 3596 <p><?php esc_html_e( 'Download a backup of your database as a ZIP file. Select which tables to include.', 'vigilante' ); ?></p>
2643 3597 <button type="button" class="button vigilante-db-backup-toggle">
@@ -2676,8 +3630,15 @@
2676 3630 </div>
2677 3631 </div>
2678 3632 </div>
2679 3633 </div>
3634 + <?php else : ?>
3635 + <div class="vigilante-tool-card vigilante-tool-card-wide">
3636 + <h3><?php esc_html_e( 'Database Backup', 'vigilante' ); ?></h3>
3637 + <p><?php esc_html_e( 'Download a backup of your database as a ZIP file. Select which tables to include.', 'vigilante' ); ?></p>
3638 + <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>
3639 + </div>
3640 + <?php endif; ?>
2680 3641 </div>
2681 3642 <?php
2682 3643 }
2683 3644
@@ -2700,14 +3661,21 @@
2700 3661 <?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 3662 </p>
2702 3663 </div>
2703 3664
3665 + <?php $vg_main_locked = $this->main_site_files_locked(); ?>
3666 + <?php if ( $vg_main_locked ) : ?>
3667 + <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;">
3668 + <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>
3669 + </div>
3670 + <?php endif; ?>
3671 +
2704 3672 <table class="form-table">
2705 3673 <tr>
2706 3674 <th scope="row"><?php esc_html_e( 'Block Bad Query Strings', 'vigilante' ); ?></th>
2707 3675 <td>
2708 3676 <label>
2709 - <input type="checkbox" name="firewall[block_bad_query_strings]" value="1" <?php checked( ! empty( $options['block_bad_query_strings'] ) ); ?>>
3677 + <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 3678 <?php esc_html_e( 'Block malicious query string patterns', 'vigilante' ); ?>
2711 3679 </label>
2712 3680 </td>
2713 3681 </tr>
@@ -2750,9 +3718,9 @@
2750 3718 <tr>
2751 3719 <th scope="row"><?php esc_html_e( 'Block Bad Bots', 'vigilante' ); ?></th>
2752 3720 <td>
2753 3721 <label>
2754 - <input type="checkbox" name="firewall[block_bad_bots]" value="1" <?php checked( ! empty( $options['block_bad_bots'] ) ); ?>>
3722 + <input type="checkbox" name="firewall[block_bad_bots]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( ! empty( $options['block_bad_bots'] ) ); ?>>
2755 3723 <?php esc_html_e( 'Block known malicious bots and scanners', 'vigilante' ); ?>
2756 3724 </label>
2757 3725 </td>
2758 3726 </tr>
@@ -2769,11 +3737,11 @@
2769 3737 </label>
2770 3738 </td>
2771 3739 </tr>
2772 3740 <tr>
2773 - <th scope="row"><?php esc_html_e( 'Requests per Minute', 'vigilante' ); ?></th>
3741 + <th scope="row"><label for="vigilante-f-firewall-rate-limiting-requests-per-minute"><?php esc_html_e( 'Requests per Minute', 'vigilante' ); ?></label></th>
2774 3742 <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">
3743 + <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 3744 <p class="description">
2777 3745 <?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 3746 </p>
2779 3747 </td>
@@ -2778,11 +3746,11 @@
2778 3746 </p>
2779 3747 </td>
2780 3748 </tr>
2781 3749 <tr>
2782 - <th scope="row"><?php esc_html_e( 'Block Duration (seconds)', 'vigilante' ); ?></th>
3750 + <th scope="row"><label for="vigilante-f-firewall-rate-limiting-block-duration"><?php esc_html_e( 'Block Duration (seconds)', 'vigilante' ); ?></label></th>
2783 3751 <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">
3752 + <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 3753 </td>
2786 3754 </tr>
2787 3755 <tr>
2788 3756 <th scope="row"><?php esc_html_e( 'Progressive Blocking', 'vigilante' ); ?></th>
@@ -2805,11 +3773,11 @@
2805 3773 </p>
2806 3774 </td>
2807 3775 </tr>
2808 3776 <tr>
2809 - <th scope="row"><?php esc_html_e( 'Maximum Block Duration', 'vigilante' ); ?></th>
3777 + <th scope="row"><label for="vigilante-f-firewall-rate-limiting-max-block-duration"><?php esc_html_e( 'Maximum Block Duration', 'vigilante' ); ?></label></th>
2810 3778 <td>
2811 - <select name="firewall[rate_limiting][max_block_duration]">
3779 + <select id="vigilante-f-firewall-rate-limiting-max-block-duration" name="firewall[rate_limiting][max_block_duration]">
2812 3780 <?php
2813 3781 $max_options = array(
2814 3782 3600 => __( '1 hour', 'vigilante' ),
2815 3783 21600 => __( '6 hours', 'vigilante' ),
@@ -2866,8 +3834,29 @@
2866 3834 </table>
2867 3835 </div>
2868 3836 <?php endif; ?>
2869 3837
3838 + <?php
3839 + // Since 2.11.8 X-Forwarded-For is read from its end, where the proxy
3840 + // writes. The administrator's own request shows whether that end is
3841 + // a CDN or a balancer for everybody here. Cross review of 2.11.8.
3842 + $xff_readings = $this->forwarded_chain_readings();
3843 + if ( $xff_readings ) :
3844 + ?>
3845 + <div id="vigilante-xff-chain-notice" class="notice notice-warning inline" style="margin:10px 0 16px;padding:8px 12px;">
3846 + <p style="margin:0;">
3847 + <?php
3848 + printf(
3849 + /* translators: 1: last address in the header, the one Vigilant reads, 2: first address in the header, which a visitor can write */
3850 + 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' ),
3851 + esc_html( $xff_readings['now'] ),
3852 + esc_html( $xff_readings['before'] )
3853 + );
3854 + ?>
3855 + </p>
3856 + </div>
3857 + <?php endif; ?>
3858 +
2870 3859 <h3><?php esc_html_e( 'IP Lists', 'vigilante' ); ?></h3>
2871 3860 <p class="description">
2872 3861 <?php
2873 3862 printf(
@@ -2878,12 +3867,12 @@
2878 3867 ?>
2879 3868 </p>
2880 3869 <table class="form-table">
2881 3870 <tr>
2882 - <th scope="row"><?php esc_html_e( 'Visitor IP detection', 'vigilante' ); ?></th>
3871 + <th scope="row"><label for="vigilante-f-firewall-trusted-proxy-header"><?php esc_html_e( 'Visitor IP detection', 'vigilante' ); ?></label></th>
2883 3872 <td>
2884 3873 <?php $proxy_header = $options['trusted_proxy_header'] ?? ''; ?>
2885 - <select name="firewall[trusted_proxy_header]">
3874 + <select id="vigilante-f-firewall-trusted-proxy-header" name="firewall[trusted_proxy_header]" <?php disabled( $vg_main_locked ); ?>>
2886 3875 <option value="" <?php selected( $proxy_header, '' ); ?>><?php esc_html_e( 'Direct connection, only REMOTE_ADDR (recommended)', 'vigilante' ); ?></option>
2887 3876 <option value="cf-connecting-ip" <?php selected( $proxy_header, 'cf-connecting-ip' ); ?>><?php esc_html_e( 'Behind Cloudflare (CF-Connecting-IP)', 'vigilante' ); ?></option>
2888 3877 <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 3878 <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 +3882,25 @@
2893 3882 </p>
2894 3883 </td>
2895 3884 </tr>
2896 3885 <tr>
2897 - <th scope="row"><?php esc_html_e( 'IP Whitelist', 'vigilante' ); ?></th>
3886 + <th scope="row"><label for="vigilante-f-firewall-trusted-proxies"><?php esc_html_e( 'Trusted proxy IPs', 'vigilante' ); ?></label></th>
2898 3887 <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>
3888 + <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 3889 <p class="description">
2901 - <?php esc_html_e( 'One IP per line. These IPs will bypass firewall checks.', 'vigilante' ); ?>
3890 + <?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' ); ?>
3891 + <?php if ( in_array( $proxy_header, array( 'x-forwarded-for', 'x-real-ip' ), true ) && empty( $options['trusted_proxies'] ) ) : ?>
3892 + <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>
3893 + <?php endif; ?>
3894 + </p>
3895 + </td>
3896 + </tr>
3897 + <tr>
3898 + <th scope="row"><label for="vigilante-f-firewall-ip-whitelist"><?php esc_html_e( 'IP Whitelist', 'vigilante' ); ?></label></th>
3899 + <td>
3900 + <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>
3901 + <p class="description">
3902 + <?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 3903 <br>
2903 3904 <?php
2904 3905 printf(
2905 3906 /* translators: 1: opening <code>, 2: closing </code>. Placeholders wrap the IP, CIDR and wildcard examples. */
@@ -2911,11 +3912,11 @@
2911 3912 </p>
2912 3913 </td>
2913 3914 </tr>
2914 3915 <tr>
2915 - <th scope="row"><?php esc_html_e( 'IP Blacklist', 'vigilante' ); ?></th>
3916 + <th scope="row"><label for="vigilante-f-firewall-ip-blacklist"><?php esc_html_e( 'IP Blacklist', 'vigilante' ); ?></label></th>
2916 3917 <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>
3918 + <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 3919 <p class="description">
2919 3920 <?php esc_html_e( 'One IP per line. These IPs will be blocked immediately.', 'vigilante' ); ?>
2920 3921 <br>
2921 3922 <?php
@@ -2934,18 +3935,18 @@
2934 3935 <h3><?php esc_html_e( 'User-Agent Lists', 'vigilante' ); ?></h3>
2935 3936 <p><?php esc_html_e( 'Partial matching: enter a keyword and any User-Agent containing it will be matched.', 'vigilante' ); ?></p>
2936 3937 <table class="form-table">
2937 3938 <tr>
2938 - <th scope="row"><?php esc_html_e( 'User-Agent Whitelist', 'vigilante' ); ?></th>
3939 + <th scope="row"><label for="vigilante-f-firewall-ua-whitelist"><?php esc_html_e( 'User-Agent Whitelist', 'vigilante' ); ?></label></th>
2939 3940 <td>
2940 - <textarea name="firewall[ua_whitelist]" rows="4" class="large-text code"><?php echo esc_textarea( implode( "\n", $options['ua_whitelist'] ?? array() ) ); ?></textarea>
3941 + <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 3942 <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 3943 </td>
2943 3944 </tr>
2944 3945 <tr>
2945 - <th scope="row"><?php esc_html_e( 'User-Agent Blacklist', 'vigilante' ); ?></th>
3946 + <th scope="row"><label for="vigilante-f-firewall-ua-blacklist"><?php esc_html_e( 'User-Agent Blacklist', 'vigilante' ); ?></label></th>
2946 3947 <td>
2947 - <textarea name="firewall[ua_blacklist]" rows="4" class="large-text code"><?php echo esc_textarea( implode( "\n", $options['ua_blacklist'] ?? array() ) ); ?></textarea>
3948 + <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 3949 <p class="description"><?php esc_html_e( 'One User-Agent per line. These will be blocked immediately.', 'vigilante' ); ?></p>
2949 3950 </td>
2950 3951 </tr>
2951 3952 </table>
@@ -2950,9 +3951,16 @@
2950 3951 </tr>
2951 3952 </table>
2952 3953 </div>
2953 3954
2954 - <div id="vigilante-section-firewall-server" class="vigilante-settings-section">
3955 + <?php
3956 + $vg_shared_locked = $this->shared_files_locked();
3957 + // Paint what is actually in force, not this site's unused copy.
3958 + $vg_local_options = $options;
3959 + $options = $this->get_section_for_display( 'firewall' );
3960 + ?>
3961 + <?php $this->render_shared_files_notice(); ?>
3962 + <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 3963 <h2>
2956 3964 <?php esc_html_e( 'Server Protection', 'vigilante' ); ?>
2957 3965 <span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span>
2958 3966 </h2>
@@ -3033,8 +4041,9 @@
3033 4041 </td>
3034 4042 </tr>
3035 4043 </table>
3036 4044 </div>
4045 + <?php $options = $vg_local_options; ?>
3037 4046
3038 4047 <p class="submit vigilante-submit-buttons">
3039 4048 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
3040 4049 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
@@ -3065,18 +4074,18 @@
3065 4074 <p><?php esc_html_e( 'Brute force protection and WordPress login hardening.', 'vigilante' ); ?></p>
3066 4075
3067 4076 <table class="form-table">
3068 4077 <tr id="field-max-attempts">
3069 - <th scope="row"><?php esc_html_e( 'Max Login Attempts', 'vigilante' ); ?></th>
4078 + <th scope="row"><label for="vigilante-f-login-security-max-attempts"><?php esc_html_e( 'Max Login Attempts', 'vigilante' ); ?></label></th>
3070 4079 <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">
4080 + <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 4081 <p class="description"><?php esc_html_e( 'Number of failed attempts before lockout.', 'vigilante' ); ?></p>
3073 4082 </td>
3074 4083 </tr>
3075 4084 <tr>
3076 - <th scope="row"><?php esc_html_e( 'Lockout Duration', 'vigilante' ); ?></th>
4085 + <th scope="row"><label for="vigilante-f-login-security-lockout-duration"><?php esc_html_e( 'Lockout Duration', 'vigilante' ); ?></label></th>
3077 4086 <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">
4087 + <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 4088 <?php esc_html_e( 'minutes', 'vigilante' ); ?>
3080 4089 </td>
3081 4090 </tr>
3082 4091 <tr>
@@ -3096,17 +4105,8 @@
3096 4105 <?php esc_html_e( 'Show generic error message instead of specific errors', 'vigilante' ); ?>
3097 4106 </label>
3098 4107 </td>
3099 4108 </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 4109 <tr>
3110 4110 <th scope="row"><?php esc_html_e( 'Disable Application Passwords', 'vigilante' ); ?></th>
3111 4111 <td>
3112 4112 <label>
@@ -3134,8 +4134,11 @@
3134 4134 </p>
3135 4135 <p class="description">
3136 4136 <?php esc_html_e( 'Direct access to wp-login.php and wp-admin will return a 404 error for non-logged users.', 'vigilante' ); ?>
3137 4137 </p>
4138 + <p class="description">
4139 + <?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' ); ?>
4140 + </p>
3138 4141 </div>
3139 4142 </td>
3140 4143 </tr>
3141 4144 </table>
@@ -3224,9 +4227,9 @@
3224 4227 $two_factor = $options['two_factor'] ?? array();
3225 4228 $two_factor_enabled = ! empty( $two_factor['enabled'] );
3226 4229 ?>
3227 4230 <div class="vigilante-settings-section vigilante-lockout-section">
3228 - <h2><?php esc_html_e( 'Login Protection Status', 'vigilante' ); ?></h2>
4231 + <h2 id="vigilante-section-login-status"><?php esc_html_e( 'Login Protection Status', 'vigilante' ); ?></h2>
3229 4232
3230 4233 <table class="form-table">
3231 4234 <tr>
3232 4235 <th scope="row"><?php esc_html_e( 'Current settings', 'vigilante' ); ?></th>
@@ -3386,9 +4389,9 @@
3386 4389 $excluded = $two_factor['excluded_users'] ?? array();
3387 4390 $method = $two_factor['method'] ?? 'email';
3388 4391 $grace_days = $two_factor['grace_period_days'] ?? 3;
3389 4392 ?>
3390 - <h3>
4393 + <h3 id="vigilante-section-login-2fa">
3391 4394 <?php esc_html_e( 'Two-Factor Authentication (2FA)', 'vigilante' ); ?>
3392 4395 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
3393 4396 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
3394 4397 </h3>
@@ -3502,11 +4505,11 @@
3502 4505 </tr>
3503 4506
3504 4507 <!-- TOTP-specific: Grace period -->
3505 4508 <tr class="vigilante-2fa-totp-only" <?php echo 'totp' !== $method ? 'style="display:none;"' : ''; ?>>
3506 - <th scope="row"><?php esc_html_e( 'Grace period', 'vigilante' ); ?></th>
4509 + <th scope="row"><label for="vigilante-f-login-security-two-factor-grace-period-days"><?php esc_html_e( 'Grace period', 'vigilante' ); ?></label></th>
3507 4510 <td>
3508 - <input type="number"
4511 + <input id="vigilante-f-login-security-two-factor-grace-period-days" type="number"
3509 4512 name="login_security[two_factor][grace_period_days]"
3510 4513 value="<?php echo esc_attr( $grace_days ); ?>"
3511 4514 min="0" max="30" class="small-text">
3512 4515 <?php esc_html_e( 'days', 'vigilante' ); ?>
@@ -3515,11 +4518,11 @@
3515 4518 </tr>
3516 4519
3517 4520 <!-- Email-specific: Sender name -->
3518 4521 <tr class="vigilante-2fa-email-only" <?php echo 'email' !== $method ? 'style="display:none;"' : ''; ?>>
3519 - <th scope="row"><?php esc_html_e( 'Email sender name', 'vigilante' ); ?></th>
4522 + <th scope="row"><label for="vigilante-f-login-security-two-factor-email-from-name"><?php esc_html_e( 'Email sender name', 'vigilante' ); ?></label></th>
3520 4523 <td>
3521 - <input type="text"
4524 + <input id="vigilante-f-login-security-two-factor-email-from-name" type="text"
3522 4525 name="login_security[two_factor][email_from_name]"
3523 4526 value="<?php echo esc_attr( $two_factor['email_from_name'] ?? '' ); ?>"
3524 4527 class="regular-text vigilante-2fa-email-from"
3525 4528 placeholder="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?>">
@@ -3587,14 +4590,140 @@
3587 4590
3588 4591 /**
3589 4592 * Render security headers tab
3590 4593 */
4594 + /**
4595 + * Offer back the header settings the 2.9.8 migration wiped.
4596 + *
4597 + * Rendered outside the settings form on purpose, so its buttons can never
4598 + * submit it, and only when there is something to actually change. Shows the
4599 + * difference before anything is written: nothing is applied that the owner
4600 + * has not seen first.
4601 + *
4602 + * @since 2.10.0
4603 + */
4604 + private function render_headers_recovery_offer() {
4605 + /*
4606 + * On a network the .htaccess belongs to every site and only the main one
4607 + * writes it, so this is not a decision a subsite gets to make. Its own
4608 + * security_headers options are inert anyway: what the network serves
4609 + * comes from the file the main site owns. Without this gate a subsite
4610 + * administrator was shown a Restore button that could only ever answer
4611 + * with a permission error, which is worse than showing nothing.
4612 + */
4613 + if ( ! Vigilante_Settings::can_write_shared_files() ) {
4614 + return;
4615 + }
4616 +
4617 + if ( ! Vigilante_Htaccess_Recovery::is_available() ) {
4618 + /*
4619 + * Already restored. Offer to take it back for as long as the previous
4620 + * section is still stored: a restore that cannot be undone is a second
4621 + * irreversible change on top of the one being repaired.
4622 + */
4623 + if ( Vigilante_Htaccess_Recovery::has_undo() ) {
4624 + ?>
4625 + <div class="notice notice-info inline" id="vigilante-headers-recovery-undo">
4626 + <p>
4627 + <?php esc_html_e( 'The Security Headers settings were restored from the copy Vigilant had kept of your .htaccess.', 'vigilante' ); ?>
4628 + <button type="button" class="button button-small" id="vigilante-recovery-undo">
4629 + <?php esc_html_e( 'Undo the restore', 'vigilante' ); ?>
4630 + </button>
4631 + </p>
4632 + </div>
4633 + <?php
4634 + }
4635 +
4636 + return;
4637 + }
4638 +
4639 + $rows = Vigilante_Htaccess_Recovery::get_diff( $this->settings );
4640 +
4641 + if ( empty( $rows ) ) {
4642 + return;
4643 + }
4644 +
4645 + $snapshot = Vigilante_Htaccess_Recovery::get_snapshot();
4646 + $taken = isset( $snapshot['time'] ) ? (int) $snapshot['time'] : 0;
4647 + $block = Vigilante_Htaccess_Recovery::get_raw_block();
4648 + ?>
4649 + <div class="vigilante-settings-section" id="vigilante-headers-recovery">
4650 + <h2><?php esc_html_e( 'Recover your previous header settings', 'vigilante' ); ?></h2>
4651 + <p>
4652 + <?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' ); ?>
4653 + </p>
4654 + <?php if ( $taken ) : ?>
4655 + <p class="description">
4656 + <?php
4657 + printf(
4658 + /* translators: %s: date and time the .htaccess copy was taken. */
4659 + esc_html__( 'Copy taken on %s.', 'vigilante' ),
4660 + esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $taken ) )
4661 + );
4662 + ?>
4663 + </p>
4664 + <?php endif; ?>
4665 +
4666 + <table class="widefat striped">
4667 + <thead>
4668 + <tr>
4669 + <th scope="col"><?php esc_html_e( 'Setting', 'vigilante' ); ?></th>
4670 + <th scope="col"><?php esc_html_e( 'Now', 'vigilante' ); ?></th>
4671 + <th scope="col"><?php esc_html_e( 'Would be restored to', 'vigilante' ); ?></th>
4672 + </tr>
4673 + </thead>
4674 + <tbody>
4675 + <?php foreach ( $rows as $row ) : ?>
4676 + <tr>
4677 + <th scope="row"><?php echo esc_html( $row['label'] ); ?></th>
4678 + <td><?php echo esc_html( $row['current'] ); ?></td>
4679 + <td>
4680 + <?php echo esc_html( $row['recovered'] ); ?>
4681 + <?php if ( ! empty( $row['detail'] ) ) : ?>
4682 + <br><span class="description"><?php echo esc_html( $row['detail'] ); ?></span>
4683 + <?php endif; ?>
4684 + </td>
4685 + </tr>
4686 + <?php endforeach; ?>
4687 + </tbody>
4688 + </table>
4689 +
4690 + <p class="description">
4691 + <?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' ); ?>
4692 + </p>
4693 +
4694 + <?php if ( '' !== $block ) : ?>
4695 + <details>
4696 + <summary><?php esc_html_e( 'Show the saved .htaccess block', 'vigilante' ); ?></summary>
4697 + <textarea readonly rows="12" class="large-text code" onclick="this.select();"><?php echo esc_textarea( $block ); ?></textarea>
4698 + </details>
4699 + <?php endif; ?>
4700 +
4701 + <p class="submit vigilante-submit-buttons">
4702 + <button type="button" class="button button-primary" id="vigilante-recovery-restore">
4703 + <?php esc_html_e( 'Restore these settings', 'vigilante' ); ?>
4704 + </button>
4705 + <button type="button" class="button" id="vigilante-recovery-dismiss">
4706 + <?php esc_html_e( 'No thanks, keep what I have', 'vigilante' ); ?>
4707 + </button>
4708 + </p>
4709 + <div id="vigilante-recovery-result"></div>
4710 + </div>
4711 + <?php
4712 + }
4713 +
3591 4714 private function render_tab_headers() {
3592 4715 $is_disabled = $this->render_module_disabled_notice( 'security_headers' );
3593 - $options = $this->settings->get_section( 'security_headers' );
4716 + // Every setting on this tab ends up in .htaccess, so on a subsite the
4717 + // whole tab is somebody else's, values included.
4718 + $vg_shared_locked = $this->shared_files_locked();
4719 + $options = $this->get_section_for_display( 'security_headers' );
3594 4720 ?>
4721 + <?php $this->render_headers_recovery_offer(); ?>
4722 +
3595 4723 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="security_headers" <?php echo $is_disabled ? 'inert' : ''; ?>>
3596 - <div id="vigilante-section-headers-main" class="vigilante-settings-section">
4724 + <?php $this->render_shared_files_notice(); ?>
4725 + <div id="vigilante-section-headers-main" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
3597 4726 <h2>
3598 4727 <?php esc_html_e( 'Security Headers', 'vigilante' ); ?>
3599 4728 <span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span>
3600 4729 </h2>
@@ -3601,11 +4730,11 @@
3601 4730 <p><?php esc_html_e( 'HTTP headers sent with every response via .htaccess (mod_headers).', 'vigilante' ); ?></p>
3602 4731
3603 4732 <table class="form-table">
3604 4733 <tr>
3605 - <th scope="row"><?php esc_html_e( 'X-Frame-Options', 'vigilante' ); ?></th>
4734 + <th scope="row"><label for="vigilante-f-security-headers-x-frame-options"><?php esc_html_e( 'X-Frame-Options', 'vigilante' ); ?></label></th>
3606 4735 <td>
3607 - <select name="security_headers[x_frame_options]">
4736 + <select id="vigilante-f-security-headers-x-frame-options" name="security_headers[x_frame_options]">
3608 4737 <option value="" <?php selected( empty( $options['x_frame_options'] ) ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option>
3609 4738 <option value="SAMEORIGIN" <?php selected( $options['x_frame_options'] ?? '', 'SAMEORIGIN' ); ?>>SAMEORIGIN</option>
3610 4739 <option value="DENY" <?php selected( $options['x_frame_options'] ?? '', 'DENY' ); ?>>DENY</option>
3611 4740 </select>
@@ -3621,11 +4750,11 @@
3621 4750 </label>
3622 4751 </td>
3623 4752 </tr>
3624 4753 <tr>
3625 - <th scope="row"><?php esc_html_e( 'Referrer-Policy', 'vigilante' ); ?></th>
4754 + <th scope="row"><label for="vigilante-f-security-headers-referrer-policy"><?php esc_html_e( 'Referrer-Policy', 'vigilante' ); ?></label></th>
3626 4755 <td>
3627 - <select name="security_headers[referrer_policy]">
4756 + <select id="vigilante-f-security-headers-referrer-policy" name="security_headers[referrer_policy]">
3628 4757 <option value="" <?php selected( empty( $options['referrer_policy'] ) ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option>
3629 4758 <option value="no-referrer" <?php selected( $options['referrer_policy'] ?? '', 'no-referrer' ); ?>>no-referrer</option>
3630 4759 <option value="strict-origin-when-cross-origin" <?php selected( $options['referrer_policy'] ?? '', 'strict-origin-when-cross-origin' ); ?>>strict-origin-when-cross-origin</option>
3631 4760 <option value="same-origin" <?php selected( $options['referrer_policy'] ?? '', 'same-origin' ); ?>>same-origin</option>
@@ -3633,65 +4762,119 @@
3633 4762 </td>
3634 4763 </tr>
3635 4764 </table>
3636 4765
3637 - <h3><?php esc_html_e( 'HSTS (HTTP Strict Transport Security)', 'vigilante' ); ?></h3>
4766 + <h3 id="vigilante-section-headers-csp"><?php esc_html_e( 'Content Security Policy', 'vigilante' ); ?></h3>
3638 4767 <table class="form-table">
3639 4768 <tr>
3640 - <th scope="row"><?php esc_html_e( 'Enable HSTS', 'vigilante' ); ?></th>
4769 + <th scope="row"><?php esc_html_e( 'Enable CSP', 'vigilante' ); ?></th>
3641 4770 <td>
3642 4771 <label>
3643 - <input type="checkbox" name="security_headers[hsts][enabled]" value="1" <?php checked( ! empty( $options['hsts']['enabled'] ) ); ?>>
3644 - <?php esc_html_e( 'Force HTTPS connections', 'vigilante' ); ?>
4772 + <input type="checkbox" name="security_headers[csp][enabled]" value="1" <?php checked( ! empty( $options['csp']['enabled'] ) ); ?>>
4773 + <?php esc_html_e( 'Enable Content Security Policy', 'vigilante' ); ?>
3645 4774 </label>
3646 - <p class="description"><?php esc_html_e( '&#9888; Warning: Only enable if your site fully supports HTTPS.', 'vigilante' ); ?></p>
3647 4775 </td>
3648 4776 </tr>
3649 4777 <tr>
3650 - <th scope="row"><?php esc_html_e( 'Max Age', 'vigilante' ); ?></th>
4778 + <th scope="row"><?php esc_html_e( 'Report Only Mode', 'vigilante' ); ?></th>
3651 4779 <td>
3652 - <select name="security_headers[hsts][max_age]">
3653 - <option value="86400" <?php selected( $options['hsts']['max_age'] ?? 31536000, 86400 ); ?>><?php esc_html_e( '1 day (testing)', 'vigilante' ); ?></option>
3654 - <option value="2592000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 2592000 ); ?>><?php esc_html_e( '30 days', 'vigilante' ); ?></option>
3655 - <option value="31536000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 31536000 ); ?>><?php esc_html_e( '1 year (recommended)', 'vigilante' ); ?></option>
3656 - <option value="63072000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 63072000 ); ?>><?php esc_html_e( '2 years', 'vigilante' ); ?></option>
3657 - </select>
4780 + <label>
4781 + <input type="checkbox" name="security_headers[csp][report_only]" value="1" <?php checked( ! empty( $options['csp']['report_only'] ) ); ?>>
4782 + <?php esc_html_e( 'Report violations without blocking (for testing)', 'vigilante' ); ?>
4783 + </label>
3658 4784 </td>
3659 4785 </tr>
4786 + </table>
4787 +
4788 + <h3 id="vigilante-section-headers-force-https"><?php esc_html_e( 'HTTPS', 'vigilante' ); ?></h3>
4789 + <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>
4790 + <table class="form-table">
3660 4791 <tr>
3661 - <th scope="row"><?php esc_html_e( 'Include Subdomains', 'vigilante' ); ?></th>
4792 + <th scope="row"><?php esc_html_e( 'Redirect HTTP to HTTPS', 'vigilante' ); ?></th>
3662 4793 <td>
3663 4794 <label>
3664 - <input type="checkbox" name="security_headers[hsts][include_subdomains]" value="1" <?php checked( ! empty( $options['hsts']['include_subdomains'] ) ); ?>>
3665 - <?php esc_html_e( 'Apply HSTS to all subdomains', 'vigilante' ); ?>
4795 + <input type="checkbox" name="security_headers[redirect_http_to_https]" value="1" <?php checked( ! empty( $options['redirect_http_to_https'] ) ); ?>>
4796 + <?php esc_html_e( 'Send visitors arriving over HTTP to the HTTPS address', 'vigilante' ); ?>
3666 4797 </label>
4798 + <p class="description"><?php esc_html_e( 'Only applies when the site address is already an https:// one. On a site still published over HTTP it does nothing, so it cannot leave the site unreachable.', 'vigilante' ); ?></p>
3667 4799 </td>
3668 4800 </tr>
4801 + <tr>
4802 + <th scope="row"><?php esc_html_e( 'Fix Mixed Content', 'vigilante' ); ?></th>
4803 + <td>
4804 + <label>
4805 + <input type="checkbox" name="security_headers[fix_mixed_content]" value="1" <?php checked( ! empty( $options['fix_mixed_content'] ) ); ?>>
4806 + <?php esc_html_e( 'Rewrite this site http:// resources to https://', 'vigilante' ); ?>
4807 + </label>
4808 + <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>
4809 + </td>
4810 + </tr>
4811 + <tr id="field-upgrade-insecure-requests">
4812 + <th scope="row"><?php esc_html_e( 'Upgrade Insecure Requests', 'vigilante' ); ?></th>
4813 + <td>
4814 + <label>
4815 + <input type="checkbox" name="security_headers[upgrade_insecure_requests]" value="1" <?php checked( ! empty( $options['upgrade_insecure_requests'] ) ); ?>>
4816 + <?php esc_html_e( 'Ask browsers to upgrade every http:// request to https://', 'vigilante' ); ?>
4817 + </label>
4818 + <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>
4819 + </td>
4820 + </tr>
4821 + <tr>
4822 + <th scope="row"><?php esc_html_e( 'Rewrite Site Address on Activation', 'vigilante' ); ?></th>
4823 + <td>
4824 + <label>
4825 + <input type="checkbox" name="security_headers[force_https]" value="1" <?php checked( ! empty( $options['force_https'] ) ); ?>>
4826 + <?php esc_html_e( 'Change the WordPress and site addresses to https:// when the plugin is activated', 'vigilante' ); ?>
4827 + </label>
4828 + <p class="description"><?php esc_html_e( '&#9888; Off by default. This writes to the WordPress Address and Site Address settings, and turning the plugin off later does not undo it. It only runs on activation, and only when the site answers over HTTPS.', 'vigilante' ); ?></p>
4829 + </td>
4830 + </tr>
3669 4831 </table>
3670 4832
3671 - <h3><?php esc_html_e( 'Content Security Policy', 'vigilante' ); ?></h3>
4833 + <h3 id="vigilante-section-headers-hsts"><?php esc_html_e( 'HSTS (HTTP Strict Transport Security)', 'vigilante' ); ?></h3>
4834 + <?php $vig_home_https = ( 0 === strpos( (string) get_option( 'home' ), 'https://' ) ); ?>
4835 + <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>
4836 + <?php if ( ! $vig_home_https ) : ?>
4837 + <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>
4838 + <?php endif; ?>
3672 4839 <table class="form-table">
3673 4840 <tr>
3674 - <th scope="row"><?php esc_html_e( 'Enable CSP', 'vigilante' ); ?></th>
4841 + <th scope="row"><?php esc_html_e( 'Enable HSTS', 'vigilante' ); ?></th>
3675 4842 <td>
4843 + <?php if ( ! $vig_home_https ) : ?>
4844 + <?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. */ ?>
4845 + <input type="hidden" name="security_headers[hsts][enabled]" value="<?php echo ! empty( $options['hsts']['enabled'] ) ? '1' : '0'; ?>">
4846 + <?php endif; ?>
3676 4847 <label>
3677 - <input type="checkbox" name="security_headers[csp][enabled]" value="1" <?php checked( ! empty( $options['csp']['enabled'] ) ); ?>>
3678 - <?php esc_html_e( 'Enable Content Security Policy', 'vigilante' ); ?>
4848 + <input type="checkbox" name="security_headers[hsts][enabled]" value="1" <?php checked( ! empty( $options['hsts']['enabled'] ) ); ?> <?php disabled( ! $vig_home_https ); ?>>
4849 + <?php esc_html_e( 'Send the Strict-Transport-Security header', 'vigilante' ); ?>
3679 4850 </label>
4851 + <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>
3680 4852 </td>
3681 4853 </tr>
3682 4854 <tr>
3683 - <th scope="row"><?php esc_html_e( 'Report Only Mode', 'vigilante' ); ?></th>
4855 + <th scope="row"><label for="vigilante-f-security-headers-hsts-max-age"><?php esc_html_e( 'Max Age', 'vigilante' ); ?></label></th>
3684 4856 <td>
4857 + <select id="vigilante-f-security-headers-hsts-max-age" name="security_headers[hsts][max_age]">
4858 + <option value="86400" <?php selected( $options['hsts']['max_age'] ?? 31536000, 86400 ); ?>><?php esc_html_e( '1 day (testing)', 'vigilante' ); ?></option>
4859 + <option value="2592000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 2592000 ); ?>><?php esc_html_e( '30 days', 'vigilante' ); ?></option>
4860 + <option value="31536000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 31536000 ); ?>><?php esc_html_e( '1 year (recommended)', 'vigilante' ); ?></option>
4861 + <option value="63072000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 63072000 ); ?>><?php esc_html_e( '2 years', 'vigilante' ); ?></option>
4862 + </select>
4863 + </td>
4864 + </tr>
4865 + <tr>
4866 + <th scope="row"><?php esc_html_e( 'Include Subdomains', 'vigilante' ); ?></th>
4867 + <td>
3685 4868 <label>
3686 - <input type="checkbox" name="security_headers[csp][report_only]" value="1" <?php checked( ! empty( $options['csp']['report_only'] ) ); ?>>
3687 - <?php esc_html_e( 'Report violations without blocking (for testing)', 'vigilante' ); ?>
4869 + <input type="checkbox" name="security_headers[hsts][include_subdomains]" value="1" <?php checked( ! empty( $options['hsts']['include_subdomains'] ) ); ?>>
4870 + <?php esc_html_e( 'Apply HSTS to all subdomains', 'vigilante' ); ?>
3688 4871 </label>
3689 4872 </td>
3690 4873 </tr>
3691 4874 </table>
3692 4875
3693 - <h3><?php esc_html_e( 'Server Identity', 'vigilante' ); ?></h3>
4876 + <h3 id="vigilante-section-headers-fingerprint"><?php esc_html_e( 'Server Identity', 'vigilante' ); ?></h3>
3694 4877 <p class="description"><?php esc_html_e( 'Hide identifying information that servers expose in responses.', 'vigilante' ); ?></p>
3695 4878 <table class="form-table">
3696 4879 <tr>
3697 4880 <th scope="row"><?php esc_html_e( 'Server Signature', 'vigilante' ); ?></th>
@@ -3713,9 +4896,57 @@
3713 4896 </tr>
3714 4897 </table>
3715 4898 </div>
3716 4899
4900 + <?php $vg_cop = ( isset( $options['cross_origin_policies'] ) && is_array( $options['cross_origin_policies'] ) ) ? $options['cross_origin_policies'] : array(); ?>
4901 + <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' : ''; ?>>
4902 + <h2>
4903 + <?php esc_html_e( 'Cross-Origin Policies', 'vigilante' ); ?>
4904 + <span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span>
4905 + </h2>
4906 + <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>
4907 +
4908 + <table class="form-table">
4909 + <tr>
4910 + <th scope="row"><label for="vigilante-f-security-headers-coop"><?php esc_html_e( 'Cross-Origin-Opener-Policy (COOP)', 'vigilante' ); ?></label></th>
4911 + <td>
4912 + <select id="vigilante-f-security-headers-coop" name="security_headers[cross_origin_policies][opener_policy]">
4913 + <option value="" <?php selected( empty( $vg_cop['opener_policy'] ) ); ?>><?php esc_html_e( 'Disabled (header not sent)', 'vigilante' ); ?></option>
4914 + <option value="unsafe-none" <?php selected( $vg_cop['opener_policy'] ?? '', 'unsafe-none' ); ?>>unsafe-none</option>
4915 + <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>
4916 + <option value="same-origin" <?php selected( $vg_cop['opener_policy'] ?? '', 'same-origin' ); ?>>same-origin</option>
4917 + </select>
4918 + <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>
4919 + </td>
4920 + </tr>
4921 + <tr>
4922 + <th scope="row"><label for="vigilante-f-security-headers-coep"><?php esc_html_e( 'Cross-Origin-Embedder-Policy (COEP)', 'vigilante' ); ?></label></th>
4923 + <td>
4924 + <select id="vigilante-f-security-headers-coep" name="security_headers[cross_origin_policies][embedder_policy]">
4925 + <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>
4926 + <option value="credentialless" <?php selected( $vg_cop['embedder_policy'] ?? '', 'credentialless' ); ?>>credentialless</option>
4927 + <option value="require-corp" <?php selected( $vg_cop['embedder_policy'] ?? '', 'require-corp' ); ?>>require-corp</option>
4928 + </select>
4929 + <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>
4930 + </td>
4931 + </tr>
4932 + <tr>
4933 + <th scope="row"><label for="vigilante-f-security-headers-corp"><?php esc_html_e( 'Cross-Origin-Resource-Policy (CORP)', 'vigilante' ); ?></label></th>
4934 + <td>
4935 + <select id="vigilante-f-security-headers-corp" name="security_headers[cross_origin_policies][resource_policy]">
4936 + <option value="" <?php selected( empty( $vg_cop['resource_policy'] ) ); ?>><?php esc_html_e( 'Disabled (header not sent)', 'vigilante' ); ?></option>
4937 + <option value="same-site" <?php selected( $vg_cop['resource_policy'] ?? '', 'same-site' ); ?>>same-site</option>
4938 + <option value="same-origin" <?php selected( $vg_cop['resource_policy'] ?? '', 'same-origin' ); ?>>same-origin</option>
4939 + <option value="cross-origin" <?php selected( $vg_cop['resource_policy'] ?? '', 'cross-origin' ); ?>><?php esc_html_e( 'cross-origin (recommended)', 'vigilante' ); ?></option>
4940 + </select>
4941 + <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>
4942 + </td>
4943 + </tr>
4944 + </table>
4945 + </div>
4946 +
3717 4947 <p class="submit vigilante-submit-buttons">
4948 + <?php if ( ! $vg_shared_locked ) : ?>
3718 4949 <button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>">
3719 4950 <?php esc_html_e( 'Save Settings', 'vigilante' ); ?>
3720 4951 </button>
3721 4952 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
@@ -3720,8 +4951,10 @@
3720 4951 </button>
3721 4952 <button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>">
3722 4953 <?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?>
3723 4954 </button>
4955 + <?php endif; ?>
4956 + <?php /* Testing what the server actually sends is read-only and useful from any site of a network. */ ?>
3724 4957 <button type="button" class="button vigilante-test-headers">
3725 4958 <?php esc_html_e( 'Test Headers', 'vigilante' ); ?>
3726 4959 </button>
3727 4960 </p>
@@ -3747,11 +4980,11 @@
3747 4980 <p><?php esc_html_e( 'Control access to WordPress REST API endpoints.', 'vigilante' ); ?></p>
3748 4981
3749 4982 <table class="form-table">
3750 4983 <tr>
3751 - <th scope="row"><?php esc_html_e( 'Access Mode', 'vigilante' ); ?></th>
4984 + <th scope="row"><label for="vigilante-f-rest-api-security-mode"><?php esc_html_e( 'Access Mode', 'vigilante' ); ?></label></th>
3752 4985 <td>
3753 - <select name="rest_api_security[mode]">
4986 + <select id="vigilante-f-rest-api-security-mode" name="rest_api_security[mode]">
3754 4987 <option value="open" <?php selected( $options['mode'] ?? 'selective', 'open' ); ?>><?php esc_html_e( 'Open - Allow all requests', 'vigilante' ); ?></option>
3755 4988 <option value="selective" <?php selected( $options['mode'] ?? 'selective', 'selective' ); ?>><?php esc_html_e( 'Selective - Protect sensitive endpoints', 'vigilante' ); ?></option>
3756 4989 <option value="authenticated_only" <?php selected( $options['mode'] ?? 'selective', 'authenticated_only' ); ?>><?php esc_html_e( 'Authenticated - Require login for all', 'vigilante' ); ?></option>
3757 4990 </select>
@@ -3830,14 +5063,14 @@
3830 5063 <?php
3831 5064 $pw_policy = wp_parse_args(
3832 5065 ( isset( $options['password_policy'] ) && is_array( $options['password_policy'] ) ) ? $options['password_policy'] : array(),
3833 5066 array(
3834 - 'require_uppercase' => true,
3835 - 'require_lowercase' => true,
3836 - 'require_number' => true,
3837 - 'require_special' => true,
5067 + 'require_uppercase' => false,
5068 + 'require_lowercase' => false,
5069 + 'require_number' => false,
5070 + 'require_special' => false,
3838 5071 'block_common' => true,
3839 - 'block_username' => false,
5072 + 'block_username' => true,
3840 5073 'affected_roles' => array(),
3841 5074 )
3842 5075 );
3843 5076 $pw_policy_roles = (array) $pw_policy['affected_roles'];
@@ -3851,11 +5084,11 @@
3851 5084 </label>
3852 5085 </td>
3853 5086 </tr>
3854 5087 <tr>
3855 - <th scope="row"><?php esc_html_e( 'Minimum Password Length', 'vigilante' ); ?></th>
5088 + <th scope="row"><label for="vigilante-f-user-security-min-password-length"><?php esc_html_e( 'Minimum Password Length', 'vigilante' ); ?></label></th>
3856 5089 <td>
3857 - <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">
5090 + <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">
3858 5091 <?php esc_html_e( 'characters', 'vigilante' ); ?>
3859 5092 </td>
3860 5093 </tr>
3861 5094 <tr>
@@ -4008,11 +5241,11 @@
4008 5241 <p class="description"><?php esc_html_e( 'Disable on high-traffic sites to avoid email overload.', 'vigilante' ); ?></p>
4009 5242 </td>
4010 5243 </tr>
4011 5244 <tr>
4012 - <th scope="row"><?php esc_html_e( 'Auto-reject After', 'vigilante' ); ?></th>
5245 + <th scope="row"><label for="vigilante-f-user-security-registration-approval-auto-reject-days"><?php esc_html_e( 'Auto-reject After', 'vigilante' ); ?></label></th>
4013 5246 <td>
4014 - <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">
5247 + <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">
4015 5248 <?php esc_html_e( 'days (0 = never)', 'vigilante' ); ?>
4016 5249 <p class="description"><?php esc_html_e( 'Automatically reject pending registrations after this many days.', 'vigilante' ); ?></p>
4017 5250 </td>
4018 5251 </tr>
@@ -4026,8 +5259,16 @@
4026 5259 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4027 5260 </h2>
4028 5261 <p><?php esc_html_e( 'Limit the number of simultaneous sessions per user.', 'vigilante' ); ?></p>
4029 5262
5263 + <?php if ( Vigilante_User_Security::session_limit_is_network_wide() ) : ?>
5264 + <div class="notice notice-warning inline">
5265 + <p>
5266 + <?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' ); ?>
5267 + </p>
5268 + </div>
5269 + <?php endif; ?>
5270 +
4030 5271 <table class="form-table">
4031 5272 <tr>
4032 5273 <th scope="row"><?php esc_html_e( 'Enable Session Limits', 'vigilante' ); ?></th>
4033 5274 <td>
@@ -4037,18 +5278,18 @@
4037 5278 </label>
4038 5279 </td>
4039 5280 </tr>
4040 5281 <tr>
4041 - <th scope="row"><?php esc_html_e( 'Maximum Sessions', 'vigilante' ); ?></th>
5282 + <th scope="row"><label for="vigilante-f-user-security-session-limits-max-sessions"><?php esc_html_e( 'Maximum Sessions', 'vigilante' ); ?></label></th>
4042 5283 <td>
4043 - <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">
5284 + <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">
4044 5285 <?php esc_html_e( 'sessions per user', 'vigilante' ); ?>
4045 5286 </td>
4046 5287 </tr>
4047 5288 <tr>
4048 - <th scope="row"><?php esc_html_e( 'When Limit Exceeded', 'vigilante' ); ?></th>
5289 + <th scope="row"><label for="vigilante-f-user-security-session-limits-behavior"><?php esc_html_e( 'When Limit Exceeded', 'vigilante' ); ?></label></th>
4049 5290 <td>
4050 - <select name="user_security[session_limits][behavior]">
5291 + <select id="vigilante-f-user-security-session-limits-behavior" name="user_security[session_limits][behavior]">
4051 5292 <option value="block_new" <?php selected( ( $session_limits['behavior'] ?? 'close_oldest' ), 'block_new' ); ?>><?php esc_html_e( 'Block new login', 'vigilante' ); ?></option>
4052 5293 <option value="close_oldest" <?php selected( ( $session_limits['behavior'] ?? 'close_oldest' ), 'close_oldest' ); ?>><?php esc_html_e( 'Close oldest session', 'vigilante' ); ?></option>
4053 5294 </select>
4054 5295 <p class="description"><?php esc_html_e( '"Close oldest" is recommended for security - ensures attackers cannot lock out legitimate users.', 'vigilante' ); ?></p>
@@ -4084,27 +5325,27 @@
4084 5325 </label>
4085 5326 </td>
4086 5327 </tr>
4087 5328 <tr>
4088 - <th scope="row"><?php esc_html_e( 'Expire After', 'vigilante' ); ?></th>
5329 + <th scope="row"><label for="vigilante-f-user-security-password-expiration-expire-days"><?php esc_html_e( 'Expire After', 'vigilante' ); ?></label></th>
4089 5330 <td>
4090 - <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">
5331 + <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">
4091 5332 <?php esc_html_e( 'days', 'vigilante' ); ?>
4092 5333 <p class="description"><?php esc_html_e( 'PCI-DSS recommends 90 days.', 'vigilante' ); ?></p>
4093 5334 </td>
4094 5335 </tr>
4095 5336 <tr>
4096 - <th scope="row"><?php esc_html_e( 'Warning Period', 'vigilante' ); ?></th>
5337 + <th scope="row"><label for="vigilante-f-user-security-password-expiration-warning-days"><?php esc_html_e( 'Warning Period', 'vigilante' ); ?></label></th>
4097 5338 <td>
4098 - <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">
5339 + <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">
4099 5340 <?php esc_html_e( 'days before expiration', 'vigilante' ); ?>
4100 5341 <p class="description"><?php esc_html_e( 'Show warning notice this many days before password expires.', 'vigilante' ); ?></p>
4101 5342 </td>
4102 5343 </tr>
4103 5344 <tr>
4104 - <th scope="row"><?php esc_html_e( 'Password History', 'vigilante' ); ?></th>
5345 + <th scope="row"><label for="vigilante-f-user-security-password-expiration-password-history"><?php esc_html_e( 'Password History', 'vigilante' ); ?></label></th>
4105 5346 <td>
4106 - <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">
5347 + <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">
4107 5348 <?php esc_html_e( 'passwords to remember', 'vigilante' ); ?>
4108 5349 <p class="description"><?php esc_html_e( 'Prevent reusing recent passwords. Set to 0 to disable.', 'vigilante' ); ?></p>
4109 5350 </td>
4110 5351 </tr>
@@ -4186,11 +5427,11 @@
4186 5427 </label>
4187 5428 </td>
4188 5429 </tr>
4189 5430 <tr>
4190 - <th scope="row"><?php esc_html_e( 'Link Expiration', 'vigilante' ); ?></th>
5431 + <th scope="row"><label for="vigilante-f-user-security-email-verification-token-expiry-hours"><?php esc_html_e( 'Link Expiration', 'vigilante' ); ?></label></th>
4191 5432 <td>
4192 - <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">
5433 + <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">
4193 5434 <?php esc_html_e( 'hours', 'vigilante' ); ?>
4194 5435 </td>
4195 5436 </tr>
4196 5437 <tr>
@@ -4202,11 +5443,11 @@
4202 5443 </label>
4203 5444 </td>
4204 5445 </tr>
4205 5446 <tr>
4206 - <th scope="row"><?php esc_html_e( 'Auto-delete Unverified', 'vigilante' ); ?></th>
5447 + <th scope="row"><label for="vigilante-f-user-security-email-verification-auto-delete-days"><?php esc_html_e( 'Auto-delete Unverified', 'vigilante' ); ?></label></th>
4207 5448 <td>
4208 - <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">
5449 + <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">
4209 5450 <?php esc_html_e( 'days (0 = never)', 'vigilante' ); ?>
4210 5451 <p class="description"><?php esc_html_e( 'Automatically delete users who never verify their email.', 'vigilante' ); ?></p>
4211 5452 </td>
4212 5453 </tr>
@@ -4230,8 +5471,11 @@
4230 5471 <h2 class="vigilante-tools-header">
4231 5472 <?php esc_html_e( 'User security tools', 'vigilante' ); ?>
4232 5473 </h2>
4233 5474
5475 + <?php $this->render_user_actions_notice(); ?>
5476 + <?php if ( ! $this->user_actions_locked() ) : ?>
5477 +
4234 5478 <!-- Force Password Reset -->
4235 5479 <div class="vigilante-tool-box">
4236 5480 <h3><?php esc_html_e( 'Force password reset', 'vigilante' ); ?></h3>
4237 5481 <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>
@@ -4364,12 +5608,20 @@
4364 5608 </div>
4365 5609
4366 5610 <!-- Pending Registrations -->
4367 5611 <?php
4368 - $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
5612 + // Enforcement-only: this instance exists to read the queue, and the
5613 + // flag keeps it from registering the module's own hooks a second
5614 + // time. It is not inert, and saying it was would be a false comment:
5615 + // init_enforcement_hooks() does add its three filters again, on top
5616 + // of the ones already registered. They are idempotent (the same
5617 + // methods of an equivalent instance, deciding on the same user meta),
5618 + // so running them twice in an admin request changes nothing, which is
5619 + // why this is accepted rather than worked around.
5620 + $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log, true );
4369 5621 $pending_users = $user_security->get_pending_users();
4370 5622 ?>
4371 - <div class="vigilante-tool-box vigilante-pending-users-section">
5623 + <div id="vigilante-section-users-pending" class="vigilante-tool-box vigilante-pending-users-section">
4372 5624 <h3>
4373 5625 <?php esc_html_e( 'Pending registrations', 'vigilante' ); ?>
4374 5626 <?php if ( count( $pending_users ) > 0 ) : ?>
4375 5627 <span class="vigilante-badge vigilante-badge-warning"><?php echo esc_html( count( $pending_users ) ); ?></span>
@@ -4375,9 +5627,20 @@
4375 5627 <span class="vigilante-badge vigilante-badge-warning"><?php echo esc_html( count( $pending_users ) ); ?></span>
4376 5628 <?php endif; ?>
4377 5629 </h3>
4378 5630
4379 - <?php if ( empty( $registration['enabled'] ) ) : ?>
5631 + <?php
5632 + /*
5633 + * The queue is shown whenever there is somebody in it, even with
5634 + * the feature off. Since 2.11.10 an account already waiting stays
5635 + * blocked when the feature is switched off, which is the point:
5636 + * turning a setting off must not quietly let in people an
5637 + * administrator decided not to approve. But hiding the table then
5638 + * left them locked out with no button anywhere to approve or
5639 + * reject them. Found by the cross review of 2.11.10.
5640 + */
5641 + ?>
5642 + <?php if ( empty( $registration['enabled'] ) && empty( $pending_users ) ) : ?>
4380 5643 <p class="description">
4381 5644 <span class="dashicons dashicons-info" style="color: #72aee6;"></span>
4382 5645 <?php esc_html_e( 'Registration approval is disabled. Enable it in the settings above to require manual approval for new users.', 'vigilante' ); ?>
4383 5646 </p>
@@ -4386,8 +5649,9 @@
4386 5649 <span class="dashicons dashicons-yes-alt"></span>
4387 5650 <p><?php esc_html_e( 'No pending registrations.', 'vigilante' ); ?></p>
4388 5651 </div>
4389 5652 <?php else : ?>
5653 + <?php $this->render_user_actions_notice(); ?>
4390 5654 <table class="wp-list-table widefat fixed striped vigilante-pending-users-table">
4391 5655 <thead>
4392 5656 <tr>
4393 5657 <th><?php esc_html_e( 'User', 'vigilante' ); ?></th>
@@ -4397,9 +5661,9 @@
4397 5661 </tr>
4398 5662 </thead>
4399 5663 <tbody>
4400 5664 <?php foreach ( $pending_users as $pending_user ) :
4401 - $pending_since = get_user_meta( $pending_user->ID, 'vigilante_pending_since', true );
5665 + $pending_since = get_user_meta( $pending_user->ID, Vigilante_User_Security::site_user_meta_key( 'vigilante_pending_since' ), true );
4402 5666 ?>
4403 5667 <tr data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>">
4404 5668 <td>
4405 5669 <?php echo get_avatar( $pending_user->ID, 32 ); ?>
@@ -4416,12 +5680,12 @@
4416 5680 }
4417 5681 ?>
4418 5682 </td>
4419 5683 <td>
4420 - <button type="button" class="button button-small vigilante-approve-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>">
5684 + <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() ); ?>>
4421 5685 <?php esc_html_e( 'Approve', 'vigilante' ); ?>
4422 5686 </button>
4423 - <button type="button" class="button button-small vigilante-reject-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>" style="color: #d63638;">
5687 + <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() ); ?>>
4424 5688 <?php esc_html_e( 'Reject', 'vigilante' ); ?>
4425 5689 </button>
4426 5690 </td>
4427 5691 </tr>
@@ -4543,8 +5807,10 @@
4543 5807 </button>
4544 5808 </p>
4545 5809 </div>
4546 5810 </div>
5811 +
5812 + <?php endif; ?>
4547 5813 </div>
4548 5814 <?php
4549 5815 }
4550 5816
@@ -4556,9 +5822,11 @@
4556 5822 $options = $this->settings->get_section( 'wp_hardening' );
4557 5823 ?>
4558 5824 <form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="wp_hardening" <?php echo $is_disabled ? 'inert' : ''; ?>>
4559 5825 <!-- Database Hardening (outside form save flow - uses its own AJAX action) -->
4560 - <div id="vigilante-section-hardening-database" class="vigilante-settings-section">
5826 + <?php $vg_shared_locked = $this->shared_files_locked(); ?>
5827 + <?php $this->render_shared_files_notice(); ?>
5828 + <div id="vigilante-section-hardening-database" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
4561 5829 <h2>
4562 5830 <?php esc_html_e( 'Database Hardening', 'vigilante' ); ?>
4563 5831 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
4564 5832 <span class="vigilante-method-badge config"><?php esc_html_e( 'WP-CONFIG', 'vigilante' ); ?></span>
@@ -4570,8 +5838,14 @@
4570 5838 $current_prefix = $db_prefix->get_current_prefix();
4571 5839 $is_default = $db_prefix->is_default_prefix();
4572 5840 ?>
4573 5841
5842 + <?php if ( is_multisite() && ! $vg_shared_locked ) : ?>
5843 + <div class="notice notice-warning inline" style="margin:10px 0 16px;padding:8px 12px;">
5844 + <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>
5845 + </div>
5846 + <?php endif; ?>
5847 +
4574 5848 <table class="form-table">
4575 5849 <tr>
4576 5850 <th scope="row"><?php esc_html_e( 'Current prefix', 'vigilante' ); ?></th>
4577 5851 <td>
@@ -4626,9 +5900,16 @@
4626 5900 </table>
4627 5901 </div>
4628 5902
4629 5903 <!-- wp-config Security -->
4630 - <div id="vigilante-section-hardening-wpconfig" class="vigilante-settings-section">
5904 + <?php
5905 + $vg_shared_locked = $this->shared_files_locked();
5906 + // Paint what is actually in force, not this site's unused copy.
5907 + $vg_local_options = $options;
5908 + $options = $this->get_section_for_display( 'wp_hardening' );
5909 + ?>
5910 + <?php $this->render_shared_files_notice(); ?>
5911 + <div id="vigilante-section-hardening-wpconfig" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>>
4631 5912 <h2>
4632 5913 <?php esc_html_e( 'wp-config.php Security', 'vigilante' ); ?>
4633 5914 <span class="vigilante-method-badge config"><?php esc_html_e( 'WP-CONFIG', 'vigilante' ); ?></span>
4634 5915 </h2>
@@ -4693,10 +5974,40 @@
4693 5974 </td>
4694 5975 </tr>
4695 5976 </table>
4696 5977 </div>
5978 + <?php $options = $vg_local_options; ?>
4697 5979
4698 5980 <!-- Comment Security -->
5981 + <div id="vigilante-section-hardening-xmlrpc" class="vigilante-settings-section">
5982 + <h2>
5983 + <?php esc_html_e( 'XML-RPC', 'vigilante' ); ?>
5984 + <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
5985 + </h2>
5986 + <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>
5987 +
5988 + <table class="form-table">
5989 + <tr id="field-disable-xmlrpc">
5990 + <th scope="row"><label for="vigilante-f-wp-hardening-xmlrpc-mode"><?php esc_html_e( 'XML-RPC access', 'vigilante' ); ?></label></th>
5991 + <td>
5992 + <?php $vig_xmlrpc_mode = Vigilante_Comment_Security::resolve_xmlrpc_mode( $this->settings ); ?>
5993 + <select id="vigilante-f-wp-hardening-xmlrpc-mode" name="wp_hardening[xmlrpc_mode]">
5994 + <option value="none" <?php selected( $vig_xmlrpc_mode, 'none' ); ?>>
5995 + <?php esc_html_e( 'Leave XML-RPC enabled', 'vigilante' ); ?>
5996 + </option>
5997 + <option value="pingback" <?php selected( $vig_xmlrpc_mode, 'pingback' ); ?>>
5998 + <?php esc_html_e( 'Block the pingback methods only', 'vigilante' ); ?>
5999 + </option>
6000 + <option value="full" <?php selected( $vig_xmlrpc_mode, 'full' ); ?>>
6001 + <?php esc_html_e( 'Disable XML-RPC completely (recommended)', 'vigilante' ); ?>
6002 + </option>
6003 + </select>
6004 + <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>
6005 + </td>
6006 + </tr>
6007 + </table>
6008 + </div>
6009 +
4699 6010 <div id="vigilante-section-hardening-comments" class="vigilante-settings-section">
4700 6011 <h2>
4701 6012 <?php esc_html_e( 'Comment Security', 'vigilante' ); ?>
4702 6013 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
@@ -4738,10 +6049,10 @@
4738 6049 <label>
4739 6050 <input type="checkbox" name="wp_hardening[close_old_comments]" value="1" <?php checked( ! empty( $options['close_old_comments'] ) ); ?>>
4740 6051 <?php esc_html_e( 'Automatically close comments on old posts after', 'vigilante' ); ?>
4741 6052 </label>
4742 - <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">
4743 - <?php esc_html_e( 'days', 'vigilante' ); ?>
6053 + <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">
6054 + <label for="vigilante-f-wp-hardening-close-comments-after-days"><?php esc_html_e( 'days', 'vigilante' ); ?></label>
4744 6055 </td>
4745 6056 </tr>
4746 6057 <tr>
4747 6058 <th scope="row"><?php esc_html_e( 'Honeypot Protection', 'vigilante' ); ?></th>
@@ -4893,13 +6204,13 @@
4893 6204 <table class="form-table">
4894 6205 <tr>
4895 6206 <th scope="row"><?php esc_html_e( 'Retention', 'vigilante' ); ?></th>
4896 6207 <td>
4897 - <input type="number" name="activity_log[retention_days]" value="<?php echo esc_attr( $options['retention_days'] ?? 30 ); ?>" min="7" max="365" class="small-text">
4898 - <?php esc_html_e( 'days', 'vigilante' ); ?>
6208 + <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">
6209 + <label for="vigilante-f-activity-log-retention-days"><?php esc_html_e( 'days', 'vigilante' ); ?></label>
4899 6210 &nbsp;&nbsp;
4900 - <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">
4901 - <?php esc_html_e( 'max entries', 'vigilante' ); ?>
6211 + <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">
6212 + <label for="vigilante-f-activity-log-max-entries"><?php esc_html_e( 'max entries', 'vigilante' ); ?></label>
4902 6213 <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>
4903 6214 </td>
4904 6215 </tr>
4905 6216 <tr>
@@ -4924,14 +6235,14 @@
4924 6235 </div>
4925 6236 </td>
4926 6237 </tr>
4927 6238 <tr>
4928 - <th scope="row"><?php esc_html_e( 'Option Tracking', 'vigilante' ); ?></th>
6239 + <th scope="row"><label for="vigilante-f-activity-log-tracked-options"><?php esc_html_e( 'Option Tracking', 'vigilante' ); ?></label></th>
4929 6240 <td>
4930 6241 <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>
4931 6242 <br>
4932 6243 <label><?php esc_html_e( 'Additional options to track:', 'vigilante' ); ?></label><br>
4933 - <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>
6244 + <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>
4934 6245 <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>
4935 6246 </td>
4936 6247 </tr>
4937 6248 <tr>
@@ -4938,15 +6249,15 @@
4938 6249 <th scope="row"><?php esc_html_e( 'Exclusions', 'vigilante' ); ?></th>
4939 6250 <td>
4940 6251 <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:16px; max-width:600px;">
4941 6252 <div>
4942 - <label><?php esc_html_e( 'Excluded user IDs:', 'vigilante' ); ?></label><br>
4943 - <textarea name="activity_log[excluded_users]" rows="3" cols="25"><?php echo esc_textarea( implode( "\n", $options['excluded_users'] ?? array() ) ); ?></textarea>
6253 + <label for="vigilante-f-activity-log-excluded-users"><?php esc_html_e( 'Excluded user IDs:', 'vigilante' ); ?></label><br>
6254 + <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>
4944 6255 <p class="description"><?php esc_html_e( 'One user ID per line. Actions by these users will not be logged.', 'vigilante' ); ?></p>
4945 6256 </div>
4946 6257 <div>
4947 - <label><?php esc_html_e( 'Excluded IPs:', 'vigilante' ); ?></label><br>
4948 - <textarea name="activity_log[excluded_ips]" rows="3" cols="25"><?php echo esc_textarea( implode( "\n", $options['excluded_ips'] ?? array() ) ); ?></textarea>
6258 + <label for="vigilante-f-activity-log-excluded-ips"><?php esc_html_e( 'Excluded IPs:', 'vigilante' ); ?></label><br>
6259 + <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>
4949 6260 <p class="description"><?php esc_html_e( 'One IP per line. Requests from these IPs will not be logged.', 'vigilante' ); ?></p>
4950 6261 </div>
4951 6262 </div>
4952 6263 </td>
@@ -4995,11 +6306,11 @@
4995 6306 <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>
4996 6307 </td>
4997 6308 </tr>
4998 6309 <tr>
4999 - <th scope="row"><?php esc_html_e( 'Alert on severity', 'vigilante' ); ?></th>
6310 + <th scope="row"><label for="vigilante-f-audit-alerts-immediate-min-severity"><?php esc_html_e( 'Alert on severity', 'vigilante' ); ?></label></th>
5000 6311 <td>
5001 - <select name="audit_alerts[immediate][min_severity]">
6312 + <select id="vigilante-f-audit-alerts-immediate-min-severity" name="audit_alerts[immediate][min_severity]">
5002 6313 <option value="critical" <?php selected( $alert_severity, 'critical' ); ?>><?php esc_html_e( 'Critical only (recommended)', 'vigilante' ); ?></option>
5003 6314 <option value="warning" <?php selected( $alert_severity, 'warning' ); ?>><?php esc_html_e( 'Warning and Critical', 'vigilante' ); ?></option>
5004 6315 </select>
5005 6316 <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>
@@ -5015,11 +6326,11 @@
5015 6326 <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>
5016 6327 </td>
5017 6328 </tr>
5018 6329 <tr>
5019 - <th scope="row"><?php esc_html_e( 'Time window', 'vigilante' ); ?></th>
6330 + <th scope="row"><label for="vigilante-f-audit-alerts-threshold-window"><?php esc_html_e( 'Time window', 'vigilante' ); ?></label></th>
5020 6331 <td>
5021 - <select name="audit_alerts[threshold][window]">
6332 + <select id="vigilante-f-audit-alerts-threshold-window" name="audit_alerts[threshold][window]">
5022 6333 <option value="30m" <?php selected( $alert_window, '30m' ); ?>><?php esc_html_e( '30 minutes', 'vigilante' ); ?></option>
5023 6334 <option value="1h" <?php selected( $alert_window, '1h' ); ?>><?php esc_html_e( '1 hour', 'vigilante' ); ?></option>
5024 6335 <option value="6h" <?php selected( $alert_window, '6h' ); ?>><?php esc_html_e( '6 hours', 'vigilante' ); ?></option>
5025 6336 <option value="24h" <?php selected( $alert_window, '24h' ); ?>><?php esc_html_e( '24 hours', 'vigilante' ); ?></option>
@@ -5046,10 +6357,10 @@
5046 6357 </tr>
5047 6358 <tr>
5048 6359 <th scope="row"><?php esc_html_e( "Don't repeat alerts", 'vigilante' ); ?></th>
5049 6360 <td>
5050 - <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">
5051 - <?php esc_html_e( 'minutes', 'vigilante' ); ?>
6361 + <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">
6362 + <label for="vigilante-f-audit-alerts-cooldown-minutes"><?php esc_html_e( 'minutes', 'vigilante' ); ?></label>
5052 6363 <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>
5053 6364 </td>
5054 6365 </tr>
5055 6366
@@ -5122,10 +6433,10 @@
5122 6433 $ua_blacklist = $firewall_options['ua_blacklist'] ?? array();
5123 6434 ?>
5124 6435
5125 6436 <div class="vigilante-log-filters">
5126 - <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">
5127 - <select id="vigilante-log-type-filter">
6437 + <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">
6438 + <select id="vigilante-log-type-filter" aria-label="<?php esc_attr_e( 'Filter the log by event type', 'vigilante' ); ?>">
5128 6439 <option value=""><?php esc_html_e( 'All Types', 'vigilante' ); ?></option>
5129 6440 <option value="login"><?php esc_html_e( 'Login', 'vigilante' ); ?></option>
5130 6441 <option value="user"><?php esc_html_e( 'User', 'vigilante' ); ?></option>
5131 6442 <option value="content"><?php esc_html_e( 'Content', 'vigilante' ); ?></option>
@@ -5138,15 +6449,15 @@
5138 6449 <option value="file"><?php esc_html_e( 'File', 'vigilante' ); ?></option>
5139 6450 <option value="security"><?php esc_html_e( 'Security', 'vigilante' ); ?></option>
5140 6451 <option value="system"><?php esc_html_e( 'System', 'vigilante' ); ?></option>
5141 6452 </select>
5142 - <select id="vigilante-log-severity-filter">
6453 + <select id="vigilante-log-severity-filter" aria-label="<?php esc_attr_e( 'Filter the log by severity', 'vigilante' ); ?>">
5143 6454 <option value=""><?php esc_html_e( 'All Severities', 'vigilante' ); ?></option>
5144 6455 <option value="info"><?php esc_html_e( 'Info', 'vigilante' ); ?></option>
5145 6456 <option value="warning"><?php esc_html_e( 'Warning', 'vigilante' ); ?></option>
5146 6457 <option value="critical"><?php esc_html_e( 'Critical', 'vigilante' ); ?></option>
5147 6458 </select>
5148 - <select id="vigilante-log-method-filter">
6459 + <select id="vigilante-log-method-filter" aria-label="<?php esc_attr_e( 'Filter the log by HTTP method', 'vigilante' ); ?>">
5149 6460 <option value=""><?php esc_html_e( 'All Methods', 'vigilante' ); ?></option>
5150 6461 <option value="GET">GET</option>
5151 6462 <option value="POST">POST</option>
5152 6463 <option value="PUT">PUT</option>
@@ -5213,8 +6524,9 @@
5213 6524 'user' => (string) ( $log->user_login ?? '' ),
5214 6525 'ip' => $ip_val,
5215 6526 'user_agent' => $ua_val,
5216 6527 'request_method' => (string) $request_method,
6528 + 'request_uri' => Vigilante_Activity_Log::extract_request_uri( $log->extra_data ?? '' ),
5217 6529 'date' => (string) ( $log->created_at ?? '' ),
5218 6530 'severity' => (string) ( $log->severity ?? 'info' ),
5219 6531 'is_ip_whitelisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_whitelist, true ) ),
5220 6532 'is_ip_blacklisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_blacklist, true ) ),
@@ -5220,8 +6532,19 @@
5220 6532 'is_ip_blacklisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_blacklist, true ) ),
5221 6533 'is_ua_whitelisted' => ( '' !== $ua_val && in_array( $ua_val, $ua_whitelist, true ) ),
5222 6534 'is_ua_blacklisted' => ( '' !== $ua_val && in_array( $ua_val, $ua_blacklist, true ) ),
5223 6535 );
6536 + // Self-protection entries carry what happened, what
6537 + // it means and what to do, from the same catalogue
6538 + // the File Integrity box uses.
6539 + $vg_self_event = Vigilante_Self_Integrity_Guidance::for_log_event(
6540 + (string) ( $log->event_action ?? '' ),
6541 + $log->extra_data ?? '',
6542 + (string) ( $log->severity ?? 'info' )
6543 + );
6544 + if ( null !== $vg_self_event ) {
6545 + $details['self'] = $vg_self_event;
6546 + }
5224 6547 $display_type = isset( $type_labels[ $log->event_type ] ) ? $type_labels[ $log->event_type ] : $log->event_type;
5225 6548 $display_severity = isset( $severity_labels[ $log->severity ] ) ? $severity_labels[ $log->severity ] : $log->severity;
5226 6549 ?>
5227 6550 <tr class="vigilante-severity-<?php echo esc_attr( $log->severity ); ?>">
@@ -5264,15 +6587,439 @@
5264 6587
5265 6588 /**
5266 6589 * Render File Integrity tab
5267 6590 */
6591 + /**
6592 + * Findings of the self-check grouped by the case that explains them, worst
6593 + * first: ten modified files are one case with ten paths, not ten copies of
6594 + * the same explanation.
6595 + *
6596 + * @param array $findings Findings from the state.
6597 + * @return array
6598 + */
6599 + private function self_findings_by_case( $findings ) {
6600 + $groups = array();
6601 + foreach ( (array) $findings as $finding ) {
6602 + if ( ! is_array( $finding ) || 'info' === ( $finding['severity'] ?? '' ) ) {
6603 + continue;
6604 + }
6605 + $guidance = Vigilante_Self_Integrity_Guidance::for_finding( $finding );
6606 + $key = $guidance['key'];
6607 + if ( ! isset( $groups[ $key ] ) ) {
6608 + $guidance['files'] = array();
6609 + $guidance['severity'] = 'warning';
6610 + $groups[ $key ] = $guidance;
6611 + }
6612 + $file = isset( $finding['file'] ) ? (string) $finding['file'] : '';
6613 + if ( '' !== $file && ! in_array( $file, $groups[ $key ]['files'], true ) ) {
6614 + $groups[ $key ]['files'][] = $file;
6615 + }
6616 + if ( 'critical' === ( $finding['severity'] ?? '' ) ) {
6617 + $groups[ $key ]['severity'] = 'critical';
6618 + }
6619 + }
6620 + uasort(
6621 + $groups,
6622 + function ( $a, $b ) {
6623 + $rank = array( 'critical' => 0, 'warning' => 1 );
6624 + $ra = isset( $rank[ $a['severity'] ] ) ? $rank[ $a['severity'] ] : 2;
6625 + $rb = isset( $rank[ $b['severity'] ] ) ? $rank[ $b['severity'] ] : 2;
6626 + return $ra - $rb;
6627 + }
6628 + );
6629 + return $groups;
6630 + }
6631 +
6632 + /**
6633 + * Human label for the context that ran the last self-check.
6634 + *
6635 + * @param string $context Stored context.
6636 + * @return string
6637 + */
6638 + private function self_context_label( $context ) {
6639 + switch ( (string) $context ) {
6640 + case 'scan':
6641 + return __( 'during a file integrity scan', 'vigilante' );
6642 + case 'upgrader':
6643 + return __( 'right after updating Vigilant', 'vigilante' );
6644 + case 'version_change':
6645 + return __( 'after a version change made outside the updater', 'vigilante' );
6646 + case 'migration':
6647 + return __( 'while updating to this version', 'vigilante' );
6648 + case 'activation':
6649 + return __( 'when Vigilant was activated', 'vigilante' );
6650 + case 'watchdog':
6651 + return __( 'from the scheduled task watchdog', 'vigilante' );
6652 + }
6653 + return '';
6654 + }
6655 +
6656 + /**
6657 + * Short line for the badge of the box, the notices and the Dashboard strip.
6658 + *
6659 + * @param string $tone Tone from Vigilante_Self_Integrity::tone().
6660 + * @param array $state State.
6661 + * @return string
6662 + */
6663 + private function self_integrity_headline( $tone, $state ) {
6664 + $files = isset( $state['files_checked'] ) ? (int) $state['files_checked'] : 0;
6665 + $anchors = ( isset( $state['anchors'] ) && is_array( $state['anchors'] ) ) ? $state['anchors'] : array();
6666 + switch ( $tone ) {
6667 + case 'critical':
6668 + return __( 'Changes detected in Vigilant own files', 'vigilante' );
6669 + case 'warning':
6670 + return ( $files < 1 )
6671 + ? __( 'Vigilant could not check its own files', 'vigilante' )
6672 + : __( 'Vigilant self-protection needs your attention', 'vigilante' );
6673 + case 'off':
6674 + return __( 'Self-protection is switched off by code', 'vigilante' );
6675 + case 'none':
6676 + return __( 'Vigilant has not checked its own files yet', 'vigilante' );
6677 + }
6678 + return sprintf(
6679 + /* translators: 1: number of files verified, 2: number of references available, out of three */
6680 + __( 'Verified: %1$d files, %2$d of 3 references', 'vigilante' ),
6681 + $files,
6682 + count( array_filter( $anchors ) )
6683 + );
6684 + }
6685 +
6686 + /**
6687 + * Vigilant self-protection box: the first block of the File Integrity
6688 + * results, with its own colour by severity, what each finding means and
6689 + * how to fix it.
6690 + *
6691 + * It renders whether or not a scan has been stored, because the self-check
6692 + * also runs after every update and once a day: before 3.0.0 this lived in
6693 + * a line above the numeric cards of the last scan, where it was invisible
6694 + * and, without a stored scan, absent.
6695 + *
6696 + * @param array $fi_options File Integrity settings section.
6697 + */
6698 + /**
6699 + * Everything the screens need to say about self-protection, read once:
6700 + * the state, its findings grouped by case, and the tone that colours the
6701 + * box, the menu counter and the notices.
6702 + *
6703 + * @param array|null $fi_options File Integrity settings, read if not given.
6704 + * @return array { state, findings, groups, tone, enabled }
6705 + */
6706 + private function self_integrity_summary( $fi_options = null ) {
6707 + // Four to six screens ask for this in the same page load (menu, notice,
6708 + // box, strip, both scores). The option is cached by the core options
6709 + // layer, but the grouping and the tone are not: memoize per request.
6710 + static $cached = null;
6711 + if ( null !== $cached && ! is_array( $fi_options ) ) {
6712 + return $cached;
6713 + }
6714 + if ( ! is_array( $fi_options ) ) {
6715 + $fi_options = (array) $this->settings->get_section( 'file_integrity' );
6716 + }
6717 + $enabled = Vigilante_Self_Integrity::is_on();
6718 + $state = Vigilante_Self_Integrity::display_state();
6719 + $findings = Vigilante_Self_Integrity::state_findings( $state, $enabled );
6720 + $summary = array(
6721 + 'state' => $state,
6722 + 'findings' => $findings,
6723 + 'groups' => $this->self_findings_by_case( $findings ),
6724 + 'tone' => Vigilante_Self_Integrity::tone( $state, $enabled ),
6725 + 'enabled' => $enabled,
6726 + );
6727 + $cached = $summary;
6728 + return $summary;
6729 + }
6730 +
6731 + private function render_self_protection_box( $fi_options ) {
6732 + $summary = $this->self_integrity_summary( $fi_options );
6733 + $enabled = $summary['enabled'];
6734 + $state = $summary['state'];
6735 + $findings = $summary['findings'];
6736 + $groups = $summary['groups'];
6737 + $tone = $summary['tone'];
6738 + $total = isset( $state['last_findings_total'] ) ? (int) $state['last_findings_total'] : count( $findings );
6739 + $status = array(
6740 + 'status' => isset( $state['last_status'] ) ? (string) $state['last_status'] : '',
6741 + 'files' => isset( $state['files_checked'] ) ? (int) $state['files_checked'] : 0,
6742 + 'anchors' => ( isset( $state['anchors'] ) && is_array( $state['anchors'] ) ) ? $state['anchors'] : array(),
6743 + 'enabled' => $enabled,
6744 + 'has_run' => ! empty( $state['last_check'] ),
6745 + );
6746 +
6747 + /*
6748 + * Nothing to do, nothing to open: one line. A card with a big icon and
6749 + * folded sections for "everything is fine" is furniture, and furniture
6750 + * is what made the previous version of this invisible.
6751 + */
6752 + if ( empty( $groups ) ) {
6753 + $this->render_self_line( $tone, $state, $status );
6754 + return;
6755 + }
6756 +
6757 + $datetime_format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
6758 + $context_label = $this->self_context_label( isset( $state['last_context'] ) ? $state['last_context'] : '' );
6759 + $icon = ( 'critical' === $tone ) ? 'dashicons-shield' : 'dashicons-warning';
6760 + ?>
6761 + <div id="vigilante-section-fi-self" class="vigilante-settings-section vigilante-self-box vigilante-self-box--<?php echo esc_attr( $tone ); ?>">
6762 + <div class="vigilante-self-card">
6763 + <div class="vigilante-self-head">
6764 + <span class="vigilante-self-icon"><span class="dashicons <?php echo esc_attr( $icon ); ?>" aria-hidden="true"></span></span>
6765 + <div class="vigilante-self-head-text">
6766 + <h2><?php echo esc_html( $this->self_integrity_headline( $tone, $state ) ); ?></h2>
6767 + <p class="vigilante-self-meta">
6768 + <?php
6769 + printf(
6770 + /* translators: %s: date and time of the last self-check */
6771 + esc_html__( 'Checked on %s', 'vigilante' ),
6772 + esc_html( wp_date( $datetime_format, (int) $state['last_check'] ) )
6773 + );
6774 + if ( '' !== $context_label ) {
6775 + echo ', ' . esc_html( $context_label );
6776 + }
6777 + if ( $status['files'] > 0 ) {
6778 + echo ', ';
6779 + printf(
6780 + /* translators: 1: number of files checked, 2: number of references available, out of three */
6781 + esc_html__( '%1$d files against %2$d of 3 references', 'vigilante' ),
6782 + (int) $status['files'],
6783 + (int) count( array_filter( $status['anchors'] ) )
6784 + );
6785 + }
6786 + ?>
6787 + </p>
6788 + </div>
6789 + </div>
6790 +
6791 + <div class="vigilante-self-cases">
6792 + <?php
6793 + $first = true;
6794 + foreach ( $groups as $group ) {
6795 + $this->render_self_case( $group, $group['files'], $group['severity'], $first );
6796 + $first = false;
6797 + }
6798 +
6799 + if ( in_array( $tone, array( 'critical', 'warning' ), true ) ) {
6800 + $can_repair = class_exists( 'Vigilante_Self_Repair' ) && Vigilante_Self_Repair::can_repair();
6801 + if ( ! $can_repair ) {
6802 + if ( class_exists( 'Vigilante_Self_Repair' ) && ! Vigilante_Self_Repair::folder_is_the_distributed_one() ) {
6803 + $vg_reason = 'renamed';
6804 + } elseif ( ! wp_is_file_mod_allowed( 'capability_update_core' ) ) {
6805 + // With DISALLOW_FILE_MODS nobody can do it from the admin,
6806 + // not even a network administrator: telling a subsite admin
6807 + // to ask theirs would send them to someone equally blocked.
6808 + $vg_reason = 'no_caps';
6809 + } elseif ( is_multisite() && ! current_user_can( 'update_plugins' ) ) {
6810 + $vg_reason = 'network';
6811 + } else {
6812 + $vg_reason = 'no_caps';
6813 + }
6814 + $this->render_self_case(
6815 + array(
6816 + 'title' => __( 'How to repair it here', 'vigilante' ),
6817 + 'meaning' => __( 'This site does not let Vigilant replace its own files from this screen.', 'vigilante' ),
6818 + 'steps' => Vigilante_Self_Integrity_Guidance::manual_steps( $vg_reason ),
6819 + ),
6820 + array(),
6821 + '',
6822 + false
6823 + );
6824 + }
6825 + }
6826 + ?>
6827 + </div>
6828 +
6829 + <?php if ( $total > count( $findings ) ) : ?>
6830 + <p class="description">
6831 + <?php
6832 + printf(
6833 + /* translators: 1: findings shown, 2: findings found in total */
6834 + esc_html__( 'Showing %1$d of %2$d findings: the rest are of the same kind.', 'vigilante' ),
6835 + (int) count( $findings ),
6836 + (int) $total
6837 + );
6838 + ?>
6839 + </p>
6840 + <?php endif; ?>
6841 +
6842 + <?php
6843 + /*
6844 + * Reinstalling fixes files, not a filter someone wrote or a hook
6845 + * someone removed: the button only appears when at least one of the
6846 + * cases on screen is one a clean copy solves. Offering it next to
6847 + * "switched off by code" was telling the person to fix something
6848 + * else.
6849 + */
6850 + $vg_offer_repair = false;
6851 + foreach ( $groups as $vg_group ) {
6852 + if ( ! empty( $vg_group['repair'] ) ) {
6853 + $vg_offer_repair = true;
6854 + break;
6855 + }
6856 + }
6857 + ?>
6858 + <p class="vigilante-self-actions">
6859 + <?php if ( $vg_offer_repair && class_exists( 'Vigilante_Self_Repair' ) && Vigilante_Self_Repair::can_repair() ) : ?>
6860 + <a class="button button-primary" href="<?php echo esc_url( Vigilante_Self_Repair::action_url() ); ?>">
6861 + <?php esc_html_e( 'Repair Vigilant', 'vigilante' ); ?>
6862 + </a>
6863 + <span class="description">
6864 + <?php esc_html_e( 'It downloads a clean copy from WordPress.org, asks before changing anything, and keeps your settings and log.', 'vigilante' ); ?>
6865 + </span>
6866 + <?php else : ?>
6867 + <span class="description">
6868 + <?php esc_html_e( 'After fixing it, run a new scan with the Run Scan Now button above.', 'vigilante' ); ?>
6869 + </span>
6870 + <?php endif; ?>
6871 + </p>
6872 + </div>
6873 + </div>
6874 + <?php
6875 + }
6876 +
6877 + /**
6878 + * The same state when there is nothing to act on: one line inside the same
6879 + * card, so the first block of the results is always the same object.
6880 + *
6881 + * @param string $tone Tone from Vigilante_Self_Integrity::tone().
6882 + * @param array $state State.
6883 + * @param array $status Arguments for the guidance catalogue.
6884 + */
6885 + private function render_self_line( $tone, $state, $status ) {
6886 + $guidance = Vigilante_Self_Integrity_Guidance::for_status( $status );
6887 + $icon = ( 'ok' === $tone ) ? 'dashicons-yes-alt' : 'dashicons-shield';
6888 + ?>
6889 + <div id="vigilante-section-fi-self" class="vigilante-settings-section vigilante-self-box vigilante-self-box--<?php echo esc_attr( $tone ); ?> vigilante-self-box--quiet">
6890 + <div class="vigilante-self-card vigilante-self-card--quiet">
6891 + <p class="vigilante-self-line">
6892 + <span class="dashicons <?php echo esc_attr( $icon ); ?>" aria-hidden="true"></span>
6893 + <strong><?php esc_html_e( 'Vigilant self-protection:', 'vigilante' ); ?></strong>
6894 + <span class="vigilante-self-line-state"><?php echo esc_html( $this->self_integrity_headline( $tone, $state ) ); ?></span>
6895 + <?php if ( ! empty( $state['last_check'] ) ) : ?>
6896 + <span class="vigilante-self-line-meta">
6897 + <?php
6898 + printf(
6899 + /* translators: %s: human time difference, like "2 hours" */
6900 + esc_html__( 'checked %s ago', 'vigilante' ),
6901 + esc_html( human_time_diff( (int) $state['last_check'], time() ) )
6902 + );
6903 + ?>
6904 + </span>
6905 + <?php endif; ?>
6906 + </p>
6907 + <?php if ( ! empty( $guidance['steps'] ) && 'none' === $tone ) : ?>
6908 + <p class="description vigilante-self-line-help"><?php echo esc_html( $guidance['steps'][0] ); ?></p>
6909 + <?php endif; ?>
6910 + </div>
6911 + </div>
6912 + <?php
6913 + }
6914 +
6915 + /**
6916 + * One case of the self-protection box, as a row that opens: what it is, how
6917 + * many files, its severity, and inside, the files, what it means and what to
6918 + * do. Plain HTML details, so there is no JavaScript between a finding about
6919 + * the security plugin and the person reading it.
6920 + *
6921 + * @param array $guidance Guidance entry (title, meaning, steps).
6922 + * @param array $files Paths this case was found in.
6923 + * @param string $severity critical|warning, empty for the how-to-repair row.
6924 + * @param bool $open Whether the row starts open.
6925 + */
6926 + private function render_self_case( $guidance, $files = array(), $severity = '', $open = false ) {
6927 + $shown = array_slice( (array) $files, 0, 20 );
6928 + $hidden = count( (array) $files ) - count( $shown );
6929 + ?>
6930 + <details class="vigilante-self-case vigilante-self-case--<?php echo esc_attr( '' !== $severity ? $severity : 'plain' ); ?>"<?php echo $open ? ' open' : ''; ?>>
6931 + <summary>
6932 + <span class="vigilante-self-case-title"><?php echo esc_html( $guidance['title'] ); ?></span>
6933 + <?php if ( ! empty( $files ) ) : ?>
6934 + <span class="vigilante-self-case-count">
6935 + <?php
6936 + printf(
6937 + /* translators: %d: number of files of this finding */
6938 + esc_html( _n( '%d file', '%d files', count( (array) $files ), 'vigilante' ) ),
6939 + (int) count( (array) $files )
6940 + );
6941 + ?>
6942 + </span>
6943 + <?php endif; ?>
6944 + <?php if ( '' !== $severity ) : ?>
6945 + <span class="vigilante-self-case-severity">
6946 + <?php echo esc_html( 'critical' === $severity ? __( 'critical', 'vigilante' ) : __( 'warning', 'vigilante' ) ); ?>
6947 + </span>
6948 + <?php endif; ?>
6949 + </summary>
6950 + <div class="vigilante-self-case-body">
6951 + <?php if ( ! empty( $shown ) ) : ?>
6952 + <ul class="vigilante-self-paths">
6953 + <?php foreach ( $shown as $file ) : ?>
6954 + <li><code><?php echo esc_html( $file ); ?></code></li>
6955 + <?php endforeach; ?>
6956 + <?php if ( $hidden > 0 ) : ?>
6957 + <li>
6958 + <?php
6959 + printf(
6960 + /* translators: %d: number of additional files */
6961 + esc_html__( 'and %d more', 'vigilante' ),
6962 + (int) $hidden
6963 + );
6964 + ?>
6965 + </li>
6966 + <?php endif; ?>
6967 + </ul>
6968 + <?php endif; ?>
6969 + <p class="vigilante-self-meaning">
6970 + <strong><?php esc_html_e( 'What it means:', 'vigilante' ); ?></strong>
6971 + <?php echo esc_html( $guidance['meaning'] ); ?>
6972 + </p>
6973 + <?php if ( ! empty( $guidance['steps'] ) ) : ?>
6974 + <p class="vigilante-self-todo"><strong><?php esc_html_e( 'What to do:', 'vigilante' ); ?></strong></p>
6975 + <ol class="vigilante-self-steps">
6976 + <?php foreach ( $guidance['steps'] as $step ) : ?>
6977 + <li><?php echo esc_html( $step ); ?></li>
6978 + <?php endforeach; ?>
6979 + </ol>
6980 + <?php endif; ?>
6981 + </div>
6982 + </details>
6983 + <?php
6984 + }
6985 +
5268 6986 private function render_tab_file_integrity() {
5269 6987 $is_disabled = $this->render_module_disabled_notice( 'file_integrity' );
5270 6988 $options = $this->settings->get_section( 'file_integrity' );
6989 + // On the main site of a network the critical-file scan is the network's
6990 + // canary for a change to wp-config.php or the root .htaccess, so a
6991 + // main-site admin without network rights cannot turn it off. Since
6992 + // 2.11.8; see Vigilante_Settings::get_main_site_file_settings().
6993 + $vg_main_locked = $this->main_site_files_locked();
5271 6994 $last_scan = get_option( 'vigilante_last_integrity_scan' );
5272 6995 $last_results = get_option( 'vigilante_last_integrity_results' );
5273 6996 $ignored_files = get_option( 'vigilante_ignored_files', array() );
5274 6997
6998 + /*
6999 + * Vigilant own findings are shown in their own box, the first of the
7000 + * results, with what each one means and how to fix it. They are taken
7001 + * out of the generic tables here (and out of the counters above them)
7002 + * so the same finding is not reported twice and so no row of the
7003 + * security plugin's own files offers an Ignore button. The stored
7004 + * results keep them: the scan email reads its own section from there.
7005 + */
7006 + if ( is_array( $last_results ) ) {
7007 + foreach ( array( 'modified', 'suspicious', 'extra', 'missing' ) as $vg_bucket ) {
7008 + if ( empty( $last_results[ $vg_bucket ] ) || ! is_array( $last_results[ $vg_bucket ] ) ) {
7009 + continue;
7010 + }
7011 + $last_results[ $vg_bucket ] = array_values(
7012 + array_filter(
7013 + $last_results[ $vg_bucket ],
7014 + function ( $vg_item ) {
7015 + return ! ( is_array( $vg_item ) && 'vigilante_self' === ( $vg_item['type'] ?? '' ) );
7016 + }
7017 + )
7018 + );
7019 + }
7020 + }
7021 +
5275 7022 // Backward compat: convert old notify_on_changes to notify_level
5276 7023 $notify_level = $options['notify_level'] ?? '';
5277 7024 if ( empty( $notify_level ) ) {
5278 7025 $notify_level = ! empty( $options['notify_on_changes'] ) ? 'all' : 'disabled';
@@ -5321,11 +7068,11 @@
5321 7068 </label>
5322 7069 </td>
5323 7070 </tr>
5324 7071 <tr>
5325 - <th scope="row"><?php esc_html_e( 'Scan Frequency', 'vigilante' ); ?></th>
7072 + <th scope="row"><label for="vigilante-f-file-integrity-scan-frequency"><?php esc_html_e( 'Scan Frequency', 'vigilante' ); ?></label></th>
5326 7073 <td>
5327 - <select name="file_integrity[scan_frequency]">
7074 + <select id="vigilante-f-file-integrity-scan-frequency" name="file_integrity[scan_frequency]">
5328 7075 <option value="daily" <?php selected( $options['scan_frequency'] ?? 'daily', 'daily' ); ?>><?php esc_html_e( 'Daily', 'vigilante' ); ?></option>
5329 7076 <option value="weekly" <?php selected( $options['scan_frequency'] ?? 'daily', 'weekly' ); ?>><?php esc_html_e( 'Weekly', 'vigilante' ); ?></option>
5330 7077 </select>
5331 7078 </td>
@@ -5330,11 +7077,11 @@
5330 7077 </select>
5331 7078 </td>
5332 7079 </tr>
5333 7080 <tr>
5334 - <th scope="row"><?php esc_html_e( 'Email Notifications', 'vigilante' ); ?></th>
7081 + <th scope="row"><label for="vigilante-f-file-integrity-notify-level"><?php esc_html_e( 'Email Notifications', 'vigilante' ); ?></label></th>
5335 7082 <td>
5336 - <select name="file_integrity[notify_level]">
7083 + <select id="vigilante-f-file-integrity-notify-level" name="file_integrity[notify_level]">
5337 7084 <option value="all" <?php selected( $notify_level, 'all' ); ?>><?php esc_html_e( 'All issues (modified + suspicious)', 'vigilante' ); ?></option>
5338 7085 <option value="suspicious_only" <?php selected( $notify_level, 'suspicious_only' ); ?>><?php esc_html_e( 'Suspicious files only', 'vigilante' ); ?></option>
5339 7086 <option value="disabled" <?php selected( $notify_level, 'disabled' ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option>
5340 7087 </select>
@@ -5394,10 +7141,13 @@
5394 7141 <?php esc_html_e( 'Uploads directory (detect PHP files, double extensions, .htaccess)', 'vigilante' ); ?>
5395 7142 </label>
5396 7143 <br>
5397 7144 <label>
5398 - <input type="checkbox" name="file_integrity[scan_critical_config]" value="1" <?php checked( $options['scan_critical_config'] ?? true ); ?>>
7145 + <input type="checkbox" name="file_integrity[scan_critical_config]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( $options['scan_critical_config'] ?? true ); ?>>
5399 7146 <?php esc_html_e( 'Critical config files (wp-config.php, .htaccess baseline monitoring)', 'vigilante' ); ?>
7147 + <?php if ( $vg_main_locked ) : ?>
7148 + <span class="description" style="display:block;margin-left:24px;"><?php echo esc_html( Vigilante_Settings::get_shared_files_notice() ); ?></span>
7149 + <?php endif; ?>
5400 7150 </label>
5401 7151 <br>
5402 7152 <label>
5403 7153 <input type="checkbox" name="file_integrity[check_closed_plugins]" value="1" <?php checked( $options['check_closed_plugins'] ?? true ); ?>>
@@ -5406,19 +7156,30 @@
5406 7156 </fieldset>
5407 7157 </td>
5408 7158 </tr>
5409 7159 <tr>
5410 - <th scope="row"><?php esc_html_e( 'Excluded Paths', 'vigilante' ); ?></th>
7160 + <th scope="row"><label for="vigilante-f-file-integrity-excluded-paths"><?php esc_html_e( 'Excluded Paths', 'vigilante' ); ?></label></th>
5411 7161 <td>
5412 - <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>
5413 - <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>
7162 + <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>
7163 + <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>
5414 7164 </td>
5415 7165 </tr>
5416 7166 <tr>
5417 - <th scope="row"><?php esc_html_e( 'Excluded Extensions', 'vigilante' ); ?></th>
7167 + <th scope="row"><label for="vigilante-f-file-integrity-excluded-extensions"><?php esc_html_e( 'Excluded Extensions', 'vigilante' ); ?></label></th>
5418 7168 <td>
5419 - <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>
5420 - <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>
7169 + <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>
7170 + <p class="description">
7171 + <?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' ); ?>
7172 + <br>
7173 + <?php
7174 + printf(
7175 + /* translators: 1: opening <code>, 2: closing </code>. Placeholders wrap the scoped-extension example. */
7176 + 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' ),
7177 + '<code>',
7178 + '</code>'
7179 + ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded.
7180 + ?>
7181 + </p>
5421 7182 </td>
5422 7183 </tr>
5423 7184 </table>
5424 7185 </div>
@@ -5441,8 +7202,10 @@
5441 7202 </form>
5442 7203
5443 7204 <div id="vigilante-scan-results" class="vigilante-settings-section" style="display:none;"></div>
5444 7205
7206 + <?php $this->render_self_protection_box( $options ); ?>
7207 +
5445 7208 <?php if ( $last_scan || $has_closed || $closed_last_check > 0 ) : ?>
5446 7209 <div id="vigilante-section-fi-last-scan" class="vigilante-settings-section">
5447 7210 <h2><?php esc_html_e( 'Last Scan Results', 'vigilante' ); ?></h2>
5448 7211 <?php if ( $last_scan ) : ?>
@@ -5567,8 +7330,11 @@
5567 7330 }
5568 7331 } else {
5569 7332 $file_path = (string) $item;
5570 7333 }
7334 + if ( 'vigilante_self' === $file_type ) {
7335 + $file_type = __( 'Vigilant (self)', 'vigilante' );
7336 + }
5571 7337 ?>
5572 7338 <tr>
5573 7339 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th>
5574 7340 <td><code style="color: #d63638;"><?php echo esc_html( $file_path ); ?></code></td>
@@ -5608,8 +7374,11 @@
5608 7374 foreach ( $last_results['extra'] as $item ) {
5609 7375 $file_path = is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item;
5610 7376 $file_reason = is_array( $item ) ? ( $item['reason'] ?? __( 'Unknown', 'vigilante' ) ) : __( 'Unknown', 'vigilante' );
5611 7377 $file_type = is_array( $item ) ? ( $item['type'] ?? 'unknown' ) : 'unknown';
7378 + if ( 'vigilante_self' === $file_type ) {
7379 + $file_type = __( 'Vigilant (self)', 'vigilante' );
7380 + }
5612 7381 ?>
5613 7382 <tr>
5614 7383 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th>
5615 7384 <td><code style="color: #b32d2e;"><?php echo esc_html( $file_path ); ?></code></td>
@@ -5640,8 +7409,19 @@
5640 7409 $regular_modified[] = $item;
5641 7410 }
5642 7411 }
5643 7412 }
7413 + // A missing file of Vigilant is critical, and the missing
7414 + // files of the generic scan have no table: it is listed with
7415 + // the modified files, or the tab said "All files passed" with
7416 + // a module deleted.
7417 + if ( $last_results && ! empty( $last_results['missing'] ) && is_array( $last_results['missing'] ) ) {
7418 + foreach ( $last_results['missing'] as $item ) {
7419 + if ( is_array( $item ) && 'vigilante_self' === ( $item['type'] ?? '' ) ) {
7420 + $regular_modified[] = $item;
7421 + }
7422 + }
7423 + }
5644 7424 ?>
5645 7425
5646 7426 <?php if ( ! empty( $critical_modified ) ) : ?>
5647 7427 <div class="vigilante-file-list vigilante-critical-config-files">
@@ -5665,9 +7445,15 @@
5665 7445 $crit_diff = $crit_item['diff'] ?? array();
5666 7446 $crit_id = sanitize_html_class( $crit_file );
5667 7447 $added_count = is_array( $crit_diff ) ? count( $crit_diff['added'] ?? array() ) : 0;
5668 7448 $removed_count = is_array( $crit_diff ) ? count( $crit_diff['removed'] ?? array() ) : 0;
5669 - $diff_unavailable = is_array( $crit_diff ) && ! empty( $crit_diff['unavailable'] );
7449 + // The lines of a shared file are for whoever approves it. Results
7450 + // stored before 2.11.8 on the main site still carry them, so the
7451 + // screen asks too, not only the scan that wrote them.
7452 + $diff_network = ( is_array( $crit_diff ) && ! empty( $crit_diff['network'] ) ) || $this->critical_approval_locked();
7453 + $diff_rescan = is_array( $crit_diff ) && ! empty( $crit_diff['rescan'] );
7454 + $diff_redaction = is_array( $crit_diff ) && ! empty( $crit_diff['redaction'] );
7455 + $diff_unavailable = $diff_network || ( is_array( $crit_diff ) && ! empty( $crit_diff['unavailable'] ) );
5670 7456 ?>
5671 7457 <tr>
5672 7458 <td><code style="color: #e36210;"><?php echo esc_html( $crit_file ); ?></code></td>
5673 7459 <td>
@@ -5690,18 +7476,36 @@
5690 7476 <td>
5691 7477 <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' ); ?>">
5692 7478 <?php esc_html_e( 'Review changes', 'vigilante' ); ?>
5693 7479 </button>
5694 - <button type="button" class="button button-small button-primary vigilante-approve-critical-file" data-file="<?php echo esc_attr( $crit_file ); ?>">
5695 - <?php esc_html_e( 'Approve', 'vigilante' ); ?>
5696 - </button>
7480 + <?php if ( $this->critical_approval_locked() ) : ?>
7481 + <span class="description" style="display:block;margin-top:4px;">
7482 + <?php echo esc_html( $this->critical_approval_notice() ); ?>
7483 + </span>
7484 + <?php else : ?>
7485 + <button type="button" class="button button-small button-primary vigilante-approve-critical-file" data-file="<?php echo esc_attr( $crit_file ); ?>">
7486 + <?php esc_html_e( 'Approve', 'vigilante' ); ?>
7487 + </button>
7488 + <?php endif; ?>
5697 7489 </td>
5698 7490 </tr>
5699 7491 <tr id="vigilante-critical-content-<?php echo esc_attr( $crit_id ); ?>" class="vigilante-critical-content-row" style="display:none;">
5700 7492 <td colspan="3" style="padding: 0;">
5701 7493 <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;">
5702 - <?php if ( $diff_unavailable ) : ?>
7494 + <?php if ( $diff_network ) : ?>
5703 7495 <p style="color: #50575e; font-style: italic; margin: 0;">
7496 + <?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' ); ?>
7497 + </p>
7498 + <?php elseif ( $diff_rescan ) : ?>
7499 + <p style="color: #50575e; font-style: italic; margin: 0;">
7500 + <?php esc_html_e( 'Run a new scan to see the line changes of this file.', 'vigilante' ); ?>
7501 + </p>
7502 + <?php elseif ( $diff_redaction ) : ?>
7503 + <p style="color: #50575e; font-style: italic; margin: 0;">
7504 + <?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' ); ?>
7505 + </p>
7506 + <?php elseif ( $diff_unavailable ) : ?>
7507 + <p style="color: #50575e; font-style: italic; margin: 0;">
5704 7508 <?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' ); ?>
5705 7509 </p>
5706 7510 <?php elseif ( empty( $crit_diff['added'] ) && empty( $crit_diff['removed'] ) ) : ?>
5707 7511 <p style="color: #50575e; font-style: italic; margin: 0;">
@@ -5729,9 +7533,9 @@
5729 7533 <?php endif; ?>
5730 7534
5731 7535 <?php if ( $has_closed ) : ?>
5732 7536 <div class="vigilante-file-list vigilante-closed-plugins">
5733 - <h3 style="color: #d63638;"><?php esc_html_e( 'Closed + Removed Plugins', 'vigilante' ); ?></h3>
7537 + <h3 id="vigilante-section-fi-closed-plugins" style="color: #d63638;"><?php esc_html_e( 'Closed + Removed Plugins', 'vigilante' ); ?></h3>
5734 7538 <p class="description" style="color: #d63638;">
5735 7539 <?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' ); ?>
5736 7540 </p>
5737 7541 <table class="wp-list-table widefat striped">
@@ -5816,8 +7620,11 @@
5816 7620 }
5817 7621 } else {
5818 7622 $file_path = (string) $item;
5819 7623 }
7624 + if ( 'vigilante_self' === $file_type ) {
7625 + $file_type = __( 'Vigilant (self)', 'vigilante' );
7626 + }
5820 7627 ?>
5821 7628 <tr>
5822 7629 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th>
5823 7630 <td><code><?php echo esc_html( $file_path ); ?></code></td>
@@ -5831,9 +7638,16 @@
5831 7638 </table>
5832 7639 </div>
5833 7640 <?php endif; ?>
5834 7641
5835 - <?php if ( $last_results && empty( $last_results['modified'] ) && empty( $last_results['suspicious'] ) && empty( $last_results['extra'] ) && ! $has_closed ) : ?>
7642 + <?php
7643 + // "All files passed" is about every file, and Vigilant's own
7644 + // are files too: with the self-protection box in red or amber
7645 + // above, this line contradicted it (its findings no longer
7646 + // travel in the tables below).
7647 + $vg_self_alarm = in_array( $this->self_integrity_summary( $options )['tone'], array( 'critical', 'off', 'warning' ), true );
7648 + ?>
7649 + <?php if ( $last_results && ! $vg_self_alarm && empty( $critical_modified ) && empty( $regular_modified ) && empty( $last_results['suspicious'] ) && empty( $last_results['extra'] ) && ! $has_closed ) : ?>
5836 7650 <p class="vigilante-all-clear" style="color: #00a32a; font-weight: bold;">
5837 7651 <?php esc_html_e( 'Good Job! All files passed integrity check. No issues found.', 'vigilante' ); ?>
5838 7652 </p>
5839 7653 <?php endif; ?>
@@ -5940,8 +7754,15 @@
5940 7754 if ( ! current_user_can( 'manage_options' ) ) {
5941 7755 wp_die( esc_html__( 'Permission denied.', 'vigilante' ), 403 );
5942 7756 }
5943 7757
7758 + // The archive carries wp-config.php, which a whole network shares. On a
7759 + // network manage_options is held by every subsite administrator, so the
7760 + // same gate the writers use applies here.
7761 + if ( ! Vigilante_Settings::can_write_shared_files() ) {
7762 + wp_die( esc_html( Vigilante_Settings::get_shared_files_notice() ), 403 );
7763 + }
7764 +
5944 7765 $backup_manager = new Vigilante_Backup_Manager();
5945 7766 $result = $backup_manager->stream_files_zip();
5946 7767
5947 7768 // stream_files_zip() exits on success; only a WP_Error returns here.
@@ -6030,8 +7851,30 @@
6030 7851
6031 7852 // Read ONLY saved options from database (not merged with defaults)
6032 7853 $saved_options = get_option( Vigilante_Settings::OPTION_NAME, array() );
6033 7854
7855 + // What is stored before this request changes anything: the shared file
7856 + // settings this user may not change are put back from here (2.11.6).
7857 + $stored_options = $saved_options;
7858 + $locked = Vigilante_Settings::get_locked_file_settings();
7859 +
7860 + if ( isset( $locked[ $section ] ) && true === $locked[ $section ] ) {
7861 + wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
7862 + }
7863 +
7864 + // A module switch is a single key, so refusing says more than a success
7865 + // that changed nothing, and the dashboard puts the toggle back.
7866 + if ( 'modules' === $section && isset( $locked['modules'], $data['modules'] ) && is_array( $locked['modules'] ) && is_array( $data['modules'] ) ) {
7867 + foreach ( array_keys( $data['modules'] ) as $vg_module ) {
7868 + if ( in_array( sanitize_key( $vg_module ), $locked['modules'], true ) ) {
7869 + wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
7870 + }
7871 + }
7872 + }
7873 +
7874 + $rejected_ips = array();
7875 + $rejected_proxies = array();
7876 +
6034 7877 // Handle modules
6035 7878 if ( 'modules' === $section && isset( $data['modules'] ) ) {
6036 7879 if ( ! isset( $saved_options['modules'] ) ) {
6037 7880 $saved_options['modules'] = array();
@@ -6049,9 +7892,16 @@
6049 7892 $current_section = isset( $saved_options[ $section ] ) ? $saved_options[ $section ] : array();
6050 7893
6051 7894 // Process the submitted data
6052 7895 $processed = $this->process_section_data( $data[ $section ], $section_defaults, $current_section );
6053 -
7896 +
7897 + // The IP boxes are free text and, until 2.9.9, whatever was typed
7898 + // went straight into the option. An entry the matcher can never
7899 + // match still sits in a security list looking like protection,
7900 + // so the ones that cannot match are dropped and reported back
7901 + // instead of being stored in silence.
7902 + $rejected_ips = $this->filter_ip_lists( $section, $processed, $rejected_proxies );
7903 +
6054 7904 // Save the processed section
6055 7905 $saved_options[ $section ] = $processed;
6056 7906
6057 7907 // Clear active preset when any section settings change
@@ -6061,8 +7911,10 @@
6061 7911
6062 7912 // Clear cache before saving
6063 7913 wp_cache_delete( Vigilante_Settings::OPTION_NAME, 'options' );
6064 7914
7915 + $saved_options = Vigilante_Settings::keep_locked_file_settings( $saved_options, $stored_options );
7916 +
6065 7917 // Save to database
6066 7918 update_option( Vigilante_Settings::OPTION_NAME, $saved_options );
6067 7919
6068 7920 // Clear the settings cache
@@ -6111,12 +7963,84 @@
6111 7963 $login_url_result['sent']
6112 7964 );
6113 7965 }
6114 7966
7967 + if ( ! empty( $rejected_ips ) ) {
7968 + $message .= ' ' . sprintf(
7969 + /* translators: %s: comma separated list of the entries that were not saved. */
7970 + _n(
7971 + 'This entry is not a valid IP, CIDR range or wildcard, so it was not saved: %s',
7972 + 'These entries are not valid IPs, CIDR ranges or wildcards, so they were not saved: %s',
7973 + count( $rejected_ips ),
7974 + 'vigilante'
7975 + ),
7976 + implode( ', ', array_map( 'esc_html', $rejected_ips ) )
7977 + );
7978 + }
7979 +
7980 + if ( ! empty( $rejected_proxies ) ) {
7981 + $message .= ' ' . sprintf(
7982 + /* translators: %s: comma separated list of the trusted proxy entries that were not saved. */
7983 + _n(
7984 + 'A trusted proxy must be an exact IP or a CIDR range, not a wildcard, so this entry was not saved: %s',
7985 + 'A trusted proxy must be an exact IP or a CIDR range, not a wildcard, so these entries were not saved: %s',
7986 + count( $rejected_proxies ),
7987 + 'vigilante'
7988 + ),
7989 + implode( ', ', array_map( 'esc_html', $rejected_proxies ) )
7990 + );
7991 + }
7992 +
6115 7993 wp_send_json_success( $message );
6116 7994 }
6117 -
7995 +
6118 7996 /**
7997 + * Keep only the IP patterns the matcher can actually match
7998 + *
7999 + * @since 2.9.9
8000 + *
8001 + * @param string $section Section being saved.
8002 + * @param array $processed Section data, edited in place.
8003 + * @return array Entries that were dropped, for the message back to the user.
8004 + */
8005 + private function filter_ip_lists( $section, &$processed, &$rejected_proxies = array() ) {
8006 + $rejected_proxies = array();
8007 +
8008 + // Trusted proxies feed an identity decision, so only exact addresses and
8009 + // CIDR ranges belong there: a wildcard is stripped with its own message,
8010 + // never stored looking effective. The matcher ignores it anyway (see
8011 + // Vigilante_IP_Utils::in_list_ip_or_cidr), this stops it persisting.
8012 + if ( 'firewall' === $section && isset( $processed['trusted_proxies'] ) && is_array( $processed['trusted_proxies'] ) ) {
8013 + $split = Vigilante_IP_Utils::split_list_ip_or_cidr( $processed['trusted_proxies'] );
8014 + $processed['trusted_proxies'] = $split['valid'];
8015 + $rejected_proxies = $split['rejected'];
8016 + }
8017 +
8018 + $lists = array(
8019 + 'firewall' => array( 'ip_whitelist', 'ip_blacklist' ),
8020 + 'login_security' => array( 'ip_whitelist' ),
8021 + );
8022 +
8023 + if ( ! isset( $lists[ $section ] ) ) {
8024 + return array();
8025 + }
8026 +
8027 + $rejected = array();
8028 +
8029 + foreach ( $lists[ $section ] as $key ) {
8030 + if ( ! isset( $processed[ $key ] ) || ! is_array( $processed[ $key ] ) ) {
8031 + continue;
8032 + }
8033 +
8034 + $split = Vigilante_IP_Utils::split_list( $processed[ $key ] );
8035 + $processed[ $key ] = $split['valid'];
8036 + $rejected = array_merge( $rejected, $split['rejected'] );
8037 + }
8038 +
8039 + return array_values( array_unique( $rejected ) );
8040 + }
8041 +
8042 + /**
6119 8043 * Send 2FA enable notifications to users
6120 8044 *
6121 8045 * @return array Result with 'sent' and 'failed' counts.
6122 8046 */
@@ -6438,13 +8362,27 @@
6438 8362
6439 8363 // Sanitize imported data recursively
6440 8364 $imported = map_deep( $imported, 'sanitize_text_field' );
6441 8365
6442 - // Validate structure
6443 - $defaults = $this->settings->get_default_options();
6444 - $merged = array_replace_recursive( $defaults, $imported );
8366 + // Validate structure: only sections and keys of the schema survive, and
8367 + // every value takes the type of its default. Until 2.11.0 this was an
8368 + // array_replace_recursive() of the file over the defaults, so any key in
8369 + // the file, known or not, landed in vigilante_options (S7). Sections
8370 + // the file does not carry keep their defaults; a section it does carry
8371 + // replaces the default one whole, because validate_options() has
8372 + // already filled in whatever the file left out.
8373 + $defaults = $this->settings->get_default_options();
8374 + $validated = $this->settings->validate_options( $imported );
8375 + $merged = $defaults;
6445 8376
8377 + foreach ( $validated as $section => $data ) {
8378 + if ( is_array( $data ) ) {
8379 + $merged[ $section ] = $data;
8380 + }
8381 + }
8382 +
6446 8383 // Save
8384 + $merged = Vigilante_Settings::keep_locked_file_settings( $merged, get_option( Vigilante_Settings::OPTION_NAME, array() ) );
6447 8385 update_option( Vigilante_Settings::OPTION_NAME, $merged );
6448 8386 $this->settings->clear_cache();
6449 8387
6450 8388 // Re-evaluate the active preset marker. The imported config may match
@@ -6467,9 +8405,9 @@
6467 8405 if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) {
6468 8406 wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' );
6469 8407 }
6470 8408
6471 - wp_send_json_success( __( 'Settings imported successfully.', 'vigilante' ) );
8409 + wp_send_json_success( __( 'Settings imported successfully.', 'vigilante' ) . $this->locked_file_settings_message() );
6472 8410 }
6473 8411
6474 8412 /**
6475 8413 * Detect whether a vigilante_options array matches a known preset.
@@ -6572,9 +8510,11 @@
6572 8510 $preset = isset( $_POST['preset'] ) ? sanitize_key( $_POST['preset'] ) : '';
6573 8511
6574 8512 // Handle reset to defaults
6575 8513 if ( 'reset' === $preset ) {
6576 - $defaults = $this->settings->get_default_options();
8514 + $stored_options = get_option( Vigilante_Settings::OPTION_NAME, array() );
8515 + $defaults = Vigilante_Settings::get_defaults_preserving_user_data( $stored_options );
8516 + $defaults = Vigilante_Settings::keep_locked_file_settings( $defaults, $stored_options );
6577 8517 update_option( Vigilante_Settings::OPTION_NAME, $defaults );
6578 8518 $this->settings->clear_cache();
6579 8519
6580 8520 // Clear active preset
@@ -6582,9 +8522,9 @@
6582 8522
6583 8523 // Apply file changes after reset
6584 8524 $this->apply_all_file_changes( $defaults );
6585 8525
6586 - wp_send_json_success( __( 'Settings reset to defaults.', 'vigilante' ) );
8526 + wp_send_json_success( __( 'Settings reset to defaults.', 'vigilante' ) . $this->locked_file_settings_message() );
6587 8527 return;
6588 8528 }
6589 8529
6590 8530 $presets = $this->settings->get_presets();
@@ -6605,13 +8545,14 @@
6605 8545 $current = get_option( Vigilante_Settings::OPTION_NAME, array() );
6606 8546 if ( ! is_array( $current ) ) {
6607 8547 $current = array();
6608 8548 }
6609 - // Make sure all known keys exist before merging — array_replace_recursive
6610 - // does not invent keys that are missing on both sides.
6611 - $current = array_replace_recursive( $this->settings->get_default_options(), $current );
8549 + // Make sure all known keys exist before merging — the merge does not
8550 + // invent keys that are missing on both sides.
8551 + $current = Vigilante_Settings::merge_preset( $this->settings->get_default_options(), $current );
6612 8552
6613 - $merged = array_replace_recursive( $current, $preset_options );
8553 + $merged = Vigilante_Settings::merge_preset( $current, $preset_options );
8554 + $merged = Vigilante_Settings::keep_locked_file_settings( $merged, get_option( Vigilante_Settings::OPTION_NAME, array() ) );
6614 8555
6615 8556 update_option( Vigilante_Settings::OPTION_NAME, $merged );
6616 8557 $this->settings->clear_cache();
6617 8558
@@ -6620,9 +8561,9 @@
6620 8561
6621 8562 // Apply file changes after preset
6622 8563 $this->apply_all_file_changes( $merged );
6623 8564
6624 - wp_send_json_success( __( 'Preset applied successfully.', 'vigilante' ) );
8565 + wp_send_json_success( __( 'Preset applied successfully.', 'vigilante' ) . $this->locked_file_settings_message() );
6625 8566 }
6626 8567
6627 8568 /**
6628 8569 * AJAX: Reset a specific section to defaults
@@ -6639,11 +8580,13 @@
6639 8580 if ( empty( $section ) ) {
6640 8581 wp_send_json_error( __( 'No section specified.', 'vigilante' ) );
6641 8582 }
6642 8583
6643 - // Get current options and defaults
8584 + // Get current options and defaults. get_defaults_preserving_user_data()
8585 + // applies the tweaks a fresh installation gets, so the button and a new
8586 + // install agree, and keeps whatever the owner typed in.
6644 8587 $current_options = $this->settings->get_all_options();
6645 - $defaults = $this->settings->get_default_options();
8588 + $defaults = Vigilante_Settings::get_defaults_preserving_user_data( $current_options );
6646 8589
6647 8590 // Check if section exists in defaults
6648 8591 if ( ! isset( $defaults[ $section ] ) ) {
6649 8592 wp_send_json_error( __( 'Invalid section.', 'vigilante' ) );
@@ -6648,11 +8591,27 @@
6648 8591 if ( ! isset( $defaults[ $section ] ) ) {
6649 8592 wp_send_json_error( __( 'Invalid section.', 'vigilante' ) );
6650 8593 }
6651 8594
6652 - // Reset only this section to defaults
6653 - $current_options[ $section ] = $defaults[ $section ];
8595 + $new_values = $defaults[ $section ];
6654 8596
8597 + /*
8598 + * On a subsite, the settings written to wp-config.php and .htaccess are
8599 + * the main site's business. Resetting the local copy of those would only
8600 + * make this screen disagree with the file, so they are carried over
8601 + * untouched, and a section that is nothing but shared settings is not
8602 + * reset at all. On the main site, a user without network rights keeps
8603 + * the ones the shared files are built from as well (2.11.6).
8604 + */
8605 + $locked = Vigilante_Settings::get_locked_file_settings();
8606 +
8607 + if ( isset( $locked[ $section ] ) && true === $locked[ $section ] ) {
8608 + wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
8609 + }
8610 +
8611 + $current_options[ $section ] = $new_values;
8612 + $current_options = Vigilante_Settings::keep_locked_file_settings( $current_options, get_option( Vigilante_Settings::OPTION_NAME, array() ) );
8613 +
6655 8614 // Save
6656 8615 update_option( Vigilante_Settings::OPTION_NAME, $current_options );
6657 8616 $this->settings->clear_cache();
6658 8617
@@ -6734,8 +8693,19 @@
6734 8693 // Save new results
6735 8694 update_option( 'vigilante_last_integrity_scan', time() );
6736 8695 update_option( 'vigilante_last_integrity_results', $results );
6737 8696
8697 + // On the main site the scan does compute the lines of wp-config.php and
8698 + // .htaccess, for the network administrator. Somebody without network
8699 + // rights gets the change and its sizes, not the lines.
8700 + if ( $this->critical_approval_locked() && ! empty( $results['modified'] ) && is_array( $results['modified'] ) ) {
8701 + foreach ( $results['modified'] as $index => $item ) {
8702 + if ( is_array( $item ) && 'critical_config' === ( $item['type'] ?? '' ) ) {
8703 + $results['modified'][ $index ]['diff'] = Vigilante_File_Integrity::network_only_diff();
8704 + }
8705 + }
8706 + }
8707 +
6738 8708 wp_send_json_success( array(
6739 8709 'message' => __( 'Scan completed.', 'vigilante' ),
6740 8710 'results' => $results,
6741 8711 'ignored_count' => count( get_option( 'vigilante_ignored_files', array() ) ),
@@ -6769,11 +8739,46 @@
6769 8739 if ( ! current_user_can( 'manage_options' ) ) {
6770 8740 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
6771 8741 }
6772 8742
8743 + $results = get_option( 'vigilante_last_integrity_results' );
8744 + $scanned_at = get_option( 'vigilante_last_integrity_scan' );
8745 +
6773 8746 delete_option( 'vigilante_last_integrity_results' );
6774 8747 delete_option( 'vigilante_last_integrity_scan' );
6775 8748
8749 + /*
8750 + * A pending change to wp-config.php or the root .htaccess is closed by
8751 + * approving it, which takes the network. Clearing the results was one
8752 + * more way to close it without, until the next scan: the ignore list was
8753 + * shut in 2.11.8 and this button was left open, found by the cross
8754 + * review of 2.11.8. So for somebody who cannot approve, those entries
8755 + * stay and everything else goes.
8756 + */
8757 + // The findings about Vigilant's own files stay too: they report for the
8758 + // whole network, and ignoring them already takes network rights.
8759 + if ( $this->critical_approval_locked() && is_array( $results ) ) {
8760 + $kept = array();
8761 + $found = false;
8762 + foreach ( array( 'modified', 'missing', 'suspicious', 'extra' ) as $bucket ) {
8763 + $kept[ $bucket ] = array_values(
8764 + array_filter(
8765 + isset( $results[ $bucket ] ) && is_array( $results[ $bucket ] ) ? $results[ $bucket ] : array(),
8766 + function ( $item ) {
8767 + return is_array( $item ) && in_array( $item['type'] ?? '', array( 'critical_config', 'vigilante_self' ), true );
8768 + }
8769 + )
8770 + );
8771 + $found = $found || ! empty( $kept[ $bucket ] );
8772 + }
8773 +
8774 + if ( $found ) {
8775 + $results = array_merge( $results, $kept );
8776 + update_option( 'vigilante_last_integrity_results', $results );
8777 + update_option( 'vigilante_last_integrity_scan', $scanned_at ? $scanned_at : time() );
8778 + }
8779 + }
8780 +
6776 8781 if ( $this->database ) {
6777 8782 $this->database->clear_file_hashes();
6778 8783 }
6779 8784
@@ -6799,8 +8804,23 @@
6799 8804 if ( empty( $file ) ) {
6800 8805 wp_send_json_error( __( 'No file specified.', 'vigilante' ) );
6801 8806 }
6802 8807
8808 + // A change to a shared file is closed by approving it, and approving it
8809 + // takes the network. Ignoring it would close the same warning without.
8810 + if ( $this->critical_approval_locked() && in_array( $file, array( 'wp-config.php', '.htaccess' ), true ) ) {
8811 + wp_send_json_error( $this->critical_approval_notice() );
8812 + }
8813 +
8814 + // Vigilant's own files are never ignored, on any site: silencing the
8815 + // check that says the security plugin was changed is the one button
8816 + // an attacker would want on this screen. Since 3.0.0 those findings
8817 + // are not rows of these tables either, so nothing in the interface
8818 + // sends them here; this is the door, not the label.
8819 + if ( Vigilante_Self_Integrity::is_own_file_path( $file ) ) {
8820 + wp_send_json_error( __( 'Findings about Vigilant own files cannot be ignored. File Integrity explains what each one means and how to repair it.', 'vigilante' ) );
8821 + }
8822 +
6803 8823 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
6804 8824 $file_integrity->ignore_file( $file );
6805 8825
6806 8826 // Also remove the file from stored scan results so UI updates
@@ -6864,12 +8884,16 @@
6864 8884 if ( ! is_array( $raw_files ) ) {
6865 8885 wp_send_json_error( __( 'Invalid request.', 'vigilante' ) );
6866 8886 }
6867 8887
6868 - $files = array();
8888 + $files = array();
8889 + $locked = $this->critical_approval_locked();
8890 + $shared = $locked ? array( 'wp-config.php', '.htaccess' ) : array();
6869 8891 foreach ( $raw_files as $f ) {
6870 8892 $clean = sanitize_text_field( $f );
6871 - if ( '' !== $clean ) {
8893 + // Same rule as ajax_ignore_file(): the two shared files when the
8894 + // network locks them, and Vigilant's own files always.
8895 + if ( '' !== $clean && ! in_array( $clean, $shared, true ) && ! Vigilante_Self_Integrity::is_own_file_path( $clean ) ) {
6872 8896 $files[] = $clean;
6873 8897 }
6874 8898 }
6875 8899