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 | vigilante.php +331 -20 2.10.33.0.0 View file →
@@ -2,9 +2,9 @@
2 2 /**
3 3 * Plugin Name: Vigilant - 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner…
4 4 * Plugin URI: https://servicios.ayudawp.com
5 5 * Description: Complete security solution for WordPress. Firewall, 2FA, security headers, login protection, file integrity monitoring, activity logging and more.
6 - * Version: 2.10.3
6 + * Version: 3.0.0
7 7 * Author: Fernando Tellado
8 8 * Author URI: https://ayudawp.com
9 9 * Text Domain: vigilante
10 10 * Requires at least: 6.2
@@ -23,9 +23,9 @@
23 23
24 24 /**
25 25 * Plugin constants
26 26 */
27 -define( 'VIGILANTE_VERSION', '2.10.3' );
27 +define( 'VIGILANTE_VERSION', '3.0.0' );
28 28 define( 'VIGILANTE_PLUGIN_FILE', __FILE__ );
29 29 define( 'VIGILANTE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
30 30 define( 'VIGILANTE_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
31 31 define( 'VIGILANTE_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
@@ -125,8 +125,9 @@
125 125 require_once VIGILANTE_INCLUDES_DIR . 'class-https-enforcer.php';
126 126 require_once VIGILANTE_INCLUDES_DIR . 'class-rest-api-security.php';
127 127 require_once VIGILANTE_INCLUDES_DIR . 'class-user-security.php';
128 128 require_once VIGILANTE_INCLUDES_DIR . 'class-login-security.php';
129 + require_once VIGILANTE_INCLUDES_DIR . 'trait-two-factor-session.php';
129 130 require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-email.php';
130 131 require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-totp.php';
131 132 require_once VIGILANTE_INCLUDES_DIR . 'class-email-template.php';
132 133 require_once VIGILANTE_INCLUDES_DIR . 'class-comment-security.php';
@@ -135,8 +136,11 @@
135 136 require_once VIGILANTE_INCLUDES_DIR . 'class-activity-log.php';
136 137 require_once VIGILANTE_INCLUDES_DIR . 'class-audit-alerts.php';
137 138 require_once VIGILANTE_INCLUDES_DIR . 'class-file-integrity.php';
138 139 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
140 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity-guidance.php';
141 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity.php';
142 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-repair.php';
139 143 require_once VIGILANTE_INCLUDES_DIR . 'class-under-attack.php';
140 144 require_once VIGILANTE_INCLUDES_DIR . 'class-database-backup.php';
141 145 require_once VIGILANTE_INCLUDES_DIR . 'class-database-prefix.php';
142 146 require_once VIGILANTE_INCLUDES_DIR . 'class-security-analyzer.php';
@@ -157,8 +161,18 @@
157 161
158 162 // Post-Under Attack scan (one-shot, scheduled by Vigilante_Under_Attack::deactivate).
159 163 add_action( 'vigilante_under_attack_post_scan', 'vigilante_run_post_under_attack_scan' );
160 164
165 + // Self-protection: verify Vigilant's own files right after the WordPress
166 + // updater replaced them. Priority 10, ahead of the File Integrity handler
167 + // at 20, which skips Vigilant while the self-check is on. Registered outside
168 + // is_admin() because automatic updates run from cron and WP-CLI.
169 + add_action( 'upgrader_process_complete', 'vigilante_on_upgrader_process_complete', 10, 2 );
170 +
171 + // One click repair of Vigilant's own files (admin-post action).
172 + Vigilante_Self_Repair::init();
173 + add_filter( 'upgrader_post_install', 'vigilante_mark_upgrader_wrote', 10, 3 );
174 +
161 175 // Initialize core components only - modules will be initialized at init
162 176 add_action( 'init', 'vigilante_init_plugin', 1 );
163 177 }
164 178
@@ -202,8 +216,15 @@
202 216 */
203 217 public $activity_log;
204 218
205 219 /**
220 + * Self-protection instance, the only one that registers hooks
221 + *
222 + * @var Vigilante_Self_Integrity|null
223 + */
224 + public $self_integrity;
225 +
226 + /**
206 227 * Get single instance of the class
207 228 *
208 229 * @return Vigilante_Main
209 230 */
@@ -257,8 +278,13 @@
257 278 Vigilante_Backup_Manager::cleanup_legacy_files();
258 279 update_option( 'vigilante_legacy_backups_cleaned', 1, false );
259 280 }
260 281
282 + // Once (2.11.6): the copies of wp-config.php, .htaccess and robots.txt
283 + // that earlier versions kept in the options table, on this site and, on
284 + // a network, on every site of it.
285 + Vigilante_Backup_Manager::maybe_purge_stored_copies();
286 +
261 287 // One-time migration (2.9.0): add '.css' to File Integrity's excluded
262 288 // extensions on existing installs. Stylesheets are rewritten so often by
263 289 // themes and optimizer plugins that they were the main post-update false
264 290 // positive. New installs get it from the defaults; this brings existing
@@ -358,15 +384,45 @@
358 384
359 385 // User Security
360 386 if ( ! empty( $options['modules']['user_security'] ) ) {
361 387 new Vigilante_User_Security( $this->settings, $this->activity_log );
388 + } else {
389 + /*
390 + * Turning the module off must not quietly unlock the accounts it
391 + * already locked. A forced password reset and a registration waiting
392 + * for approval are marks written on somebody's account, with their
393 + * sessions already destroyed and the activity log saying they cannot
394 + * get in; until 2.11.10 both stopped being enforced the moment this
395 + * toggle went off and every one of those accounts logged in again
396 + * with its old password. Only the enforcing half is registered here.
397 + */
398 + new Vigilante_User_Security( $this->settings, $this->activity_log, true );
362 399 }
363 400
364 401 // Login Security
402 + $login_security = null;
403 +
365 404 if ( ! empty( $options['modules']['login_security'] ) ) {
366 405 $login_security = new Vigilante_Login_Security( $this->settings, $this->database, $this->activity_log );
367 -
368 - // Two-Factor Authentication (only if login security module is active)
406 + }
407 +
408 + /*
409 + * Two factor on a network is decided for the whole network, so it has to
410 + * be ENFORCED on the whole network too. Gating these two on this site's
411 + * module toggle left the last leg of the bypass open: the administrator
412 + * of any subsite can turn Login Security off on their own site, which
413 + * takes them out of the picture but not out of the network, and the
414 + * session cookie WordPress issues there is valid on every host of it. So
415 + * a login sent to that subsite registered no second factor check at all
416 + * and the cookie it handed back opened the main site. Reproduced over
417 + * HTTP by the second cross review of 2.11.10.
418 + *
419 + * Same reasoning, and the same shape, as the enforcement-only User
420 + * Security above: on a network the enforcing half is registered whatever
421 + * this site says. On a single site there is no other site to protect and
422 + * the toggle means what it says.
423 + */
424 + if ( is_multisite() || null !== $login_security ) {
369 425 new Vigilante_Two_Factor_Email( $this->settings, $this->database, $this->activity_log, $login_security );
370 426 new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log, $login_security );
371 427 }
372 428
@@ -378,10 +434,41 @@
378 434 }
379 435
380 436 // File Integrity Scanner
381 437 if ( ! empty( $options['modules']['file_integrity'] ) ) {
382 - new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log );
438 + $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log );
439 + $file_integrity->init_hooks();
440 + $file_integrity->init_cleanup_hooks();
441 +
383 442 new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
443 + } elseif ( is_admin() ) {
444 + /*
445 + * The module is off, and the cleanup goes on anyway. It is not
446 + * integrity monitoring: it takes out of the database the copy of
447 + * wp-config.php that earlier versions stored, credentials and all.
448 + * Turning the module off is not a decision to keep them.
449 + *
450 + * Two holes closed here, both reported by @calzbert after reading the
451 + * 2.11.3 diff. A site with the module off cleaned itself by neither
452 + * of its own two paths, because both hang off this class. And with
453 + * the module off on the MAIN site, the network sweep was not
454 + * registered either, which is what would have reached every other
455 + * site: the sweep removes each site's option without asking whether
456 + * the module is on over there.
457 + *
458 + * Only in the admin, because both hooks are admin_init and there is
459 + * nothing to gain from building this on a front-end request. Note
460 + * that admin-ajax.php fires admin_init too (wp-admin/admin-ajax.php
461 + * :45), so this also runs on wp_ajax_nopriv_* requests from
462 + * visitors with no session. That is deliberate and it is what the
463 + * module has been doing since 2.11.2: the cleanup asks for no
464 + * capability because it also runs under wp-cron with nobody logged
465 + * in, and all it does is take the plugin's own copy out of the
466 + * database. The network sweep, which does reach across sites, is
467 + * the one that demands manage_network_options.
468 + */
469 + $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log );
470 + $file_integrity->init_cleanup_hooks();
384 471 }
385 472
386 473 // Activity Log is always initialized (core component)
387 474 // Logging is gated by the modules.activity_log toggle and per-type flags
@@ -396,8 +483,16 @@
396 483
397 484 // Under Attack mode - always loaded (independent of modules)
398 485 new Vigilante_Under_Attack( $this->settings, $this->activity_log );
399 486
487 + // Self-protection - always loaded, NOT gated by modules.file_integrity:
488 + // the version change and watchdog paths must stay alive with the File
489 + // Integrity module off. There is no setting to gate it: see
490 + // Vigilante_Self_Integrity::is_on(). Hooks are registered here and only here;
491 + // every other new of the class is a plain object.
492 + $this->self_integrity = new Vigilante_Self_Integrity( $this->settings, $this->activity_log );
493 + $this->self_integrity->init_hooks();
494 +
400 495 // Admin interface
401 496 if ( is_admin() ) {
402 497 new Vigilante_Admin( $this->settings, $this->database, $this->activity_log );
403 498 }
@@ -464,9 +559,9 @@
464 559 * without having written a thing. The .htaccess was never refreshed after
465 560 * an update, and the one-shot snapshot behind it was consumed without
466 561 * being taken, so not even a network administrator visiting afterwards
467 562 * retried, because the version had already been marked. Reported by
468 - * calzbert, who found it reading the code.
563 + * @calzbert, who found it reading the code.
469 564 */
470 565 if ( ! Vigilante_Settings::owns_shared_files() ) {
471 566 $this->mark_server_files_synced();
472 567 return;
@@ -539,24 +634,33 @@
539 634 * state alone, so whichever request wins finishes the job and this one
540 635 * stays out of the way. Treating it as a failure would arm the one hour
541 636 * backoff for something that is already being handled.
542 637 */
543 - $locked = false;
638 + $locked = false;
639 + $incomplete = false;
640 + $unreadable = false;
641 + $settled = array( 'locked', 'block_incomplete', 'read_failed' );
544 642
545 643 if ( $needs_protection_block ) {
546 644 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php';
547 - $result = ( new Vigilante_Htaccess_Protection( $this->settings ) )->apply_rules( true );
548 - $locked = $locked || ( is_wp_error( $result ) && 'locked' === $result->get_error_code() );
549 - $failed = $failed || ( is_wp_error( $result ) && 'locked' !== $result->get_error_code() );
550 - $rewrote = true;
645 + $result = ( new Vigilante_Htaccess_Protection( $this->settings ) )->apply_rules( true );
646 + $code = is_wp_error( $result ) ? $result->get_error_code() : ( true === $result ? '' : 'unexpected_result' );
647 + $locked = $locked || 'locked' === $code;
648 + $incomplete = $incomplete || 'block_incomplete' === $code;
649 + $unreadable = $unreadable || 'read_failed' === $code;
650 + $failed = $failed || ( '' !== $code && ! in_array( $code, $settled, true ) );
651 + $rewrote = true;
551 652 }
552 653
553 654 if ( ! $locked && ! empty( $options['modules']['security_headers'] ) ) {
554 655 require_once VIGILANTE_INCLUDES_DIR . 'class-security-headers.php';
555 - $result = ( new Vigilante_Security_Headers( $this->settings ) )->apply_rules( true );
556 - $locked = $locked || ( is_wp_error( $result ) && 'locked' === $result->get_error_code() );
557 - $failed = $failed || ( is_wp_error( $result ) && 'locked' !== $result->get_error_code() );
558 - $rewrote = true;
656 + $result = ( new Vigilante_Security_Headers( $this->settings ) )->apply_rules( true );
657 + $code = is_wp_error( $result ) ? $result->get_error_code() : ( true === $result ? '' : 'unexpected_result' );
658 + $locked = $locked || 'locked' === $code;
659 + $incomplete = $incomplete || 'block_incomplete' === $code;
660 + $unreadable = $unreadable || 'read_failed' === $code;
661 + $failed = $failed || ( '' !== $code && ! in_array( $code, $settled, true ) );
662 + $rewrote = true;
559 663 }
560 664
561 665 if ( $locked ) {
562 666 return;
@@ -580,11 +684,45 @@
580 684
581 685 return;
582 686 }
583 687
688 + /*
689 + * A block with a BEGIN line and no END is not going to mend itself, so
690 + * retrying every hour would only repeat the refusal: it is recorded once
691 + * for this version, with what to do about it, and the job is marked done.
692 + * Saving the Firewall or Headers tab after fixing the file writes the
693 + * rules again.
694 + */
695 + if ( $incomplete && $this->activity_log ) {
696 + $this->activity_log->log(
697 + 'system',
698 + 'server_rules_block_incomplete',
699 + __( 'The .htaccess rules were not rewritten after the update because a Vigilant block in that file has a BEGIN line without its END, and rewriting it would have cut everything below it. Remove the broken block by hand, then save the Firewall or Headers tab.', 'vigilante' ),
700 + array( 'version' => VIGILANTE_VERSION ),
701 + 'warning'
702 + );
703 + }
704 +
705 + /*
706 + * Same for a .htaccess that PHP can write but not read: since 2.11.8 it is
707 + * left as it is rather than replaced by the Vigilant rules alone, and
708 + * that does not mend itself either. The first version of that fix left
709 + * it to the hourly retry, with a message about read only files; found by
710 + * the cross review of 2.11.8.
711 + */
712 + if ( $unreadable && $this->activity_log ) {
713 + $this->activity_log->log(
714 + 'system',
715 + 'server_rules_read_failed',
716 + __( 'The .htaccess rules were not rewritten after the update because PHP can write that file but cannot read it, and writing it without reading it would have removed every other rule in it. Let PHP read the file, then save the Firewall or Headers tab.', 'vigilante' ),
717 + array( 'version' => VIGILANTE_VERSION ),
718 + 'warning'
719 + );
720 + }
721 +
584 722 $this->mark_server_files_synced();
585 723
586 - if ( $rewrote && $this->activity_log ) {
724 + if ( $rewrote && ! $incomplete && ! $unreadable && $this->activity_log ) {
587 725 $this->activity_log->log(
588 726 'system',
589 727 'server_rules_refreshed',
590 728 sprintf(
@@ -650,10 +788,13 @@
650 788 $this->database->cleanup_expired_2fa_codes();
651 789 $this->database->cleanup_expired_trusted_devices();
652 790
653 791 // Remove sensitive files (readme.html, license.txt, licencia.txt)
654 - // WordPress core updates recreate these files, so we clean them daily
655 - $advanced = $this->settings->get_section( 'advanced' );
792 + // WordPress core updates recreate these files, so we clean them daily.
793 + // They sit in the root every site of a network shares, so only the main
794 + // site removes them, from its own settings; until 2.11.6 the daily
795 + // maintenance of any site did.
796 + $advanced = Vigilante_Settings::owns_shared_files() ? $this->settings->get_section( 'advanced' ) : array();
656 797 if ( ! empty( $advanced['remove_readme'] ) ) {
657 798 $readme_path = ABSPATH . 'readme.html';
658 799 if ( file_exists( $readme_path ) ) {
659 800 wp_delete_file( $readme_path );
@@ -668,8 +809,26 @@
668 809 }
669 810 }
670 811 }
671 812
813 + // Self-protection from cron, for sites nobody opens the admin of: the
814 + // watchdog of Vigilant's own scheduled events, and the check for a
815 + // version change made outside the WordPress updater (FTP, manual).
816 + // The version change goes first: it is the path that reports a downgrade
817 + // by email, and the watchdog's own daily check would otherwise get there
818 + // before it.
819 + if ( $this->self_integrity ) {
820 + if ( $this->self_integrity->is_enabled() ) {
821 + $this->self_integrity->detect_version_change();
822 + $this->self_integrity->run_watchdog();
823 + } else {
824 + // Switched off by code: the one line that still has to be
825 + // written, or an installation that stopped checking itself
826 + // would do it in silence.
827 + $this->self_integrity->audit_off_state();
828 + }
829 + }
830 +
672 831 // Log maintenance
673 832 $this->activity_log->log( 'system', 'maintenance', __( 'Daily maintenance completed', 'vigilante' ) );
674 833 }
675 834
@@ -779,8 +938,157 @@
779 938 $under_attack->run_analyzer_scan( 'all' );
780 939 }
781 940
782 941 /**
942 + * Verify Vigilant's own files right after the WordPress updater replaced them
943 + *
944 + * Runs as the OLD code with the NEW files already on disk, so the class reads
945 + * everything from disk (the Version header, the manifest) and nothing from
946 + * constants in memory. The first update to 3.0.0 is not seen here, because the
947 + * code that receives the hook is 2.x: that one is covered by the 3.0.0
948 + * migration block of Vigilante_Admin::run_migrations().
949 + *
950 + * Plugin_Upgrader::upgrade() passes the updated plugin in 'plugin', and
951 + * bulk_upgrade() passes the list in 'plugins'; both are read. Replacing the
952 + * plugin by uploading its zip, from the screen or with `wp plugin install
953 + * --force`, goes through Plugin_Upgrader::install() instead, whose context
954 + * names no plugin (wp-admin/includes/class-plugin-upgrader.php, install()):
955 + * plugin_info() reads it from the folder that was written.
956 + *
957 + * The check itself waits for the end of the request (see
958 + * vigilante_verify_after_upgrade()): WP_Upgrader::run() fires this hook also
959 + * when the install failed, and restores the previous copy of the plugin on
960 + * shutdown, so checking here reported the half-moved folder of a failed update
961 + * as tampering, by email.
962 + *
963 + * @since 3.0.0
964 + *
965 + * @param WP_Upgrader|mixed $upgrader Upgrader instance.
966 + * @param array $hook_extra Update context.
967 + */
968 +function vigilante_on_upgrader_process_complete( $upgrader, $hook_extra ) {
969 + if ( ! is_array( $hook_extra ) || 'plugin' !== ( isset( $hook_extra['type'] ) ? $hook_extra['type'] : '' ) ) {
970 + return;
971 + }
972 +
973 + $action = isset( $hook_extra['action'] ) ? $hook_extra['action'] : '';
974 + $files = array();
975 + if ( 'update' === $action ) {
976 + $files = ( ! empty( $hook_extra['plugins'] ) && is_array( $hook_extra['plugins'] ) ) ? $hook_extra['plugins'] : array();
977 + if ( ! empty( $hook_extra['plugin'] ) ) {
978 + $files[] = $hook_extra['plugin'];
979 + }
980 + } elseif ( 'install' === $action && is_object( $upgrader ) && method_exists( $upgrader, 'plugin_info' ) ) {
981 + $installed = $upgrader->plugin_info();
982 + if ( is_string( $installed ) && '' !== $installed ) {
983 + $files[] = $installed;
984 + }
985 + } else {
986 + return;
987 + }
988 + if ( ! in_array( VIGILANTE_PLUGIN_BASENAME, array_map( 'strval', $files ), true ) ) {
989 + return;
990 + }
991 +
992 + // After WP_Upgrader::restore_temp_backup() (shutdown, priority 10) and
993 + // before WP_Upgrader::delete_temp_backup() (priority 100).
994 + if ( false === has_action( 'shutdown', 'vigilante_verify_after_upgrade' ) ) {
995 + add_action( 'shutdown', 'vigilante_verify_after_upgrade', 50 );
996 + }
997 +}
998 +
999 +/**
1000 + * The check after an update, at the end of the request
1001 + *
1002 + * By now a failed update has had its previous copy restored, so the files
1003 + * on disk are the ones the site will run. Hooked by
1004 + * vigilante_on_upgrader_process_complete(); a bulk update with Vigilant in
1005 + * the list hooks it once.
1006 + *
1007 + * @since 3.0.0
1008 + */
1009 +function vigilante_verify_after_upgrade() {
1010 + if ( ! class_exists( 'Vigilante_Settings' ) ) {
1011 + require_once VIGILANTE_INCLUDES_DIR . 'class-settings.php';
1012 + }
1013 + if ( ! class_exists( 'Vigilante_Self_Integrity' ) ) {
1014 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity-guidance.php';
1015 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity.php';
1016 + }
1017 +
1018 + $settings = new Vigilante_Settings();
1019 + $activity_log = null;
1020 + if ( class_exists( 'Vigilante_Activity_Log' ) && class_exists( 'Vigilante_Database' ) ) {
1021 + $database = new Vigilante_Database();
1022 + $activity_log = new Vigilante_Activity_Log( $settings, $database );
1023 + }
1024 +
1025 + $self_integrity = new Vigilante_Self_Integrity( $settings, $activity_log );
1026 + $self_integrity->handle_upgrader( vigilante_upgrader_wrote_files_on_disk() );
1027 +}
1028 +
1029 +/**
1030 + * Whether the files on disk are the ones the WordPress updater wrote in this request
1031 + *
1032 + * The marker of vigilante_mark_upgrader_wrote() says WordPress wrote the
1033 + * folder, not that the folder is still that copy at the end of the request:
1034 + * the automatic updater puts the previous copy back when the updated plugin
1035 + * breaks its loopback request, and a failed install is restored on shutdown.
1036 + * The manifest identifies the copy, so the updater is trusted only when the
1037 + * manifest on disk is the one it wrote.
1038 + *
1039 + * @since 3.0.0
1040 + *
1041 + * @return bool
1042 + */
1043 +function vigilante_upgrader_wrote_files_on_disk() {
1044 + $mark = isset( $GLOBALS['vigilante_upgrader_wrote'] ) ? $GLOBALS['vigilante_upgrader_wrote'] : null;
1045 + if ( ! is_array( $mark ) || ! array_key_exists( 'manifest', $mark ) ) {
1046 + return false;
1047 + }
1048 + return $mark['manifest'] === Vigilante_Self_Integrity::manifest_fingerprint_of( VIGILANTE_PLUGIN_DIR );
1049 +}
1050 +
1051 +/**
1052 + * Remember that the WordPress updater wrote Vigilant's folder in this request
1053 + *
1054 + * upgrader_post_install runs once per package, after the files are in place
1055 + * and before upgrader_process_complete. The hook that follows names every
1056 + * plugin of a bulk update, including the ones that were skipped, so the check
1057 + * after the update only trusts the updater when this marker says it really
1058 + * replaced the folder. Only a folder of that name in the plugins directory
1059 + * counts (a theme can have the same name), and the marker keeps the
1060 + * fingerprint of the manifest that was written, which
1061 + * vigilante_upgrader_wrote_files_on_disk() compares with the disk at the end.
1062 + *
1063 + * @since 3.0.0
1064 + *
1065 + * @param bool|WP_Error $response Installation response.
1066 + * @param array $hook_extra Extra arguments passed to hooked filters.
1067 + * @param array $result Installation result data.
1068 + * @return bool|WP_Error The response, unchanged.
1069 + */
1070 +function vigilante_mark_upgrader_wrote( $response, $hook_extra, $result ) {
1071 + if ( is_wp_error( $response ) || ! is_array( $result ) || ! isset( $result['destination_name'], $result['destination'], $result['local_destination'] ) ) {
1072 + return $response;
1073 + }
1074 + if ( dirname( VIGILANTE_PLUGIN_BASENAME ) !== $result['destination_name'] ) {
1075 + return $response;
1076 + }
1077 + if ( untrailingslashit( wp_normalize_path( (string) $result['local_destination'] ) ) !== untrailingslashit( wp_normalize_path( WP_PLUGIN_DIR ) ) ) {
1078 + return $response;
1079 + }
1080 + if ( ! class_exists( 'Vigilante_Self_Integrity' ) ) {
1081 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity-guidance.php';
1082 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity.php';
1083 + }
1084 + $GLOBALS['vigilante_upgrader_wrote'] = array(
1085 + 'manifest' => Vigilante_Self_Integrity::manifest_fingerprint_of( (string) $result['destination'] ),
1086 + );
1087 + return $response;
1088 +}
1089 +
1090 +/**
783 1091 * Plugin activation hook
784 1092 */
785 1093 function vigilante_activate() {
786 1094 require_once VIGILANTE_INCLUDES_DIR . 'class-database.php';
@@ -793,16 +1101,19 @@
793 1101 register_activation_hook( __FILE__, 'vigilante_activate' );
794 1102
795 1103 /**
796 1104 * Plugin deactivation hook
1105 + *
1106 + * @param bool $network_wide Whether core is deactivating the plugin for the whole network.
797 1107 */
798 -function vigilante_deactivate() {
1108 +function vigilante_deactivate( $network_wide = false ) {
799 1109 require_once VIGILANTE_INCLUDES_DIR . 'class-database.php';
800 1110 require_once VIGILANTE_INCLUDES_DIR . 'class-settings.php';
801 1111 require_once VIGILANTE_INCLUDES_DIR . 'class-backup-manager.php';
1112 + require_once VIGILANTE_INCLUDES_DIR . 'class-wpconfig-security.php';
802 1113 require_once VIGILANTE_INCLUDES_DIR . 'class-deactivator.php';
803 1114
804 - Vigilante_Deactivator::deactivate();
1115 + Vigilante_Deactivator::deactivate( (bool) $network_wide );
805 1116 }
806 1117 register_deactivation_hook( __FILE__, 'vigilante_deactivate' );
807 1118
808 1119 /**