PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 3.0.2
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v3.0.2
3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 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 All 93 releases
← All changes | vigilante.php +387 -20 2.11.1 → 3.0.2 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.11.1
6 + * Version: 3.0.2
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.11.1' );
27 +define( 'VIGILANTE_VERSION', '3.0.2' );
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__ ) );
@@ -136,8 +136,11 @@
136 136 require_once VIGILANTE_INCLUDES_DIR . 'class-activity-log.php';
137 137 require_once VIGILANTE_INCLUDES_DIR . 'class-audit-alerts.php';
138 138 require_once VIGILANTE_INCLUDES_DIR . 'class-file-integrity.php';
139 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';
140 143 require_once VIGILANTE_INCLUDES_DIR . 'class-under-attack.php';
141 144 require_once VIGILANTE_INCLUDES_DIR . 'class-database-backup.php';
142 145 require_once VIGILANTE_INCLUDES_DIR . 'class-database-prefix.php';
143 146 require_once VIGILANTE_INCLUDES_DIR . 'class-security-analyzer.php';
@@ -158,8 +161,18 @@
158 161
159 162 // Post-Under Attack scan (one-shot, scheduled by Vigilante_Under_Attack::deactivate).
160 163 add_action( 'vigilante_under_attack_post_scan', 'vigilante_run_post_under_attack_scan' );
161 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 +
162 175 // Initialize core components only - modules will be initialized at init
163 176 add_action( 'init', 'vigilante_init_plugin', 1 );
164 177 }
165 178
@@ -203,8 +216,15 @@
203 216 */
204 217 public $activity_log;
205 218
206 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 + /**
207 227 * Get single instance of the class
208 228 *
209 229 * @return Vigilante_Main
210 230 */
@@ -258,8 +278,70 @@
258 278 Vigilante_Backup_Manager::cleanup_legacy_files();
259 279 update_option( 'vigilante_legacy_backups_cleaned', 1, false );
260 280 }
261 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 +
287 + /*
288 + * One-time (3.0.1): put back the lists that every version up to 3.0.0
289 + * blanked without being asked.
290 + *
291 + * Saving any tab wrote array() over every list of that section whose key
292 + * the submitted form did not carry, and four of those lists have no
293 + * field on any screen, so their key could never be carried. Pressing
294 + * Save on the Firewall tab emptied allowed_http_methods; pressing it on
295 + * Users emptied insecure_usernames and the roles of registration
296 + * approval. Nothing showed, because reading the settings merged the
297 + * shipped values over the top of whatever was stored.
298 + *
299 + * 3.0.1 stops doing that: an empty stored list now means empty, which is
300 + * what makes unticking every role work. On a site that had been saved,
301 + * an empty allowed_http_methods would make the firewall answer 403 to
302 + * every method, the front page included.
303 + *
304 + * So the key is removed, not filled in: with no key the setting takes
305 + * its shipped value again, which is exactly what it was worth before,
306 + * and it follows the shipped value if that ever changes. Only these
307 + * four, and only when empty. Every other list has a field on screen, so
308 + * an empty one was emptied by somebody who meant it, and it is left
309 + * alone. Runs once per site, tracked by an option and not a transient.
310 + */
311 + if ( ! get_option( 'vigilante_blank_lists_restored' ) ) {
312 + $vig_stored = get_option( Vigilante_Settings::OPTION_NAME, array() );
313 + $vig_changed = false;
314 +
315 + if ( is_array( $vig_stored ) ) {
316 + $vig_blanked = array(
317 + array( 'firewall', 'allowed_http_methods' ),
318 + array( 'user_security', 'insecure_usernames' ),
319 + array( 'rest_api_security', 'allowed_public_endpoints' ),
320 + array( 'user_security', 'registration_approval', 'affected_roles' ),
321 + );
322 +
323 + foreach ( $vig_blanked as $vig_path ) {
324 + if ( 2 === count( $vig_path ) ) {
325 + if ( isset( $vig_stored[ $vig_path[0] ][ $vig_path[1] ] ) && array() === $vig_stored[ $vig_path[0] ][ $vig_path[1] ] ) {
326 + unset( $vig_stored[ $vig_path[0] ][ $vig_path[1] ] );
327 + $vig_changed = true;
328 + }
329 + } elseif ( isset( $vig_stored[ $vig_path[0] ][ $vig_path[1] ][ $vig_path[2] ] ) && array() === $vig_stored[ $vig_path[0] ][ $vig_path[1] ][ $vig_path[2] ] ) {
330 + unset( $vig_stored[ $vig_path[0] ][ $vig_path[1] ][ $vig_path[2] ] );
331 + $vig_changed = true;
332 + }
333 + }
334 + }
335 +
336 + if ( $vig_changed ) {
337 + update_option( Vigilante_Settings::OPTION_NAME, $vig_stored );
338 + $this->settings->clear_cache();
339 + }
340 +
341 + update_option( 'vigilante_blank_lists_restored', 1, false );
342 + }
343 +
262 344 // One-time migration (2.9.0): add '.css' to File Integrity's excluded
263 345 // extensions on existing installs. Stylesheets are rewritten so often by
264 346 // themes and optimizer plugins that they were the main post-update false
265 347 // positive. New installs get it from the defaults; this brings existing
@@ -359,15 +441,45 @@
359 441
360 442 // User Security
361 443 if ( ! empty( $options['modules']['user_security'] ) ) {
362 444 new Vigilante_User_Security( $this->settings, $this->activity_log );
445 + } else {
446 + /*
447 + * Turning the module off must not quietly unlock the accounts it
448 + * already locked. A forced password reset and a registration waiting
449 + * for approval are marks written on somebody's account, with their
450 + * sessions already destroyed and the activity log saying they cannot
451 + * get in; until 2.11.10 both stopped being enforced the moment this
452 + * toggle went off and every one of those accounts logged in again
453 + * with its old password. Only the enforcing half is registered here.
454 + */
455 + new Vigilante_User_Security( $this->settings, $this->activity_log, true );
363 456 }
364 457
365 458 // Login Security
459 + $login_security = null;
460 +
366 461 if ( ! empty( $options['modules']['login_security'] ) ) {
367 462 $login_security = new Vigilante_Login_Security( $this->settings, $this->database, $this->activity_log );
368 -
369 - // Two-Factor Authentication (only if login security module is active)
463 + }
464 +
465 + /*
466 + * Two factor on a network is decided for the whole network, so it has to
467 + * be ENFORCED on the whole network too. Gating these two on this site's
468 + * module toggle left the last leg of the bypass open: the administrator
469 + * of any subsite can turn Login Security off on their own site, which
470 + * takes them out of the picture but not out of the network, and the
471 + * session cookie WordPress issues there is valid on every host of it. So
472 + * a login sent to that subsite registered no second factor check at all
473 + * and the cookie it handed back opened the main site. Reproduced over
474 + * HTTP by the second cross review of 2.11.10.
475 + *
476 + * Same reasoning, and the same shape, as the enforcement-only User
477 + * Security above: on a network the enforcing half is registered whatever
478 + * this site says. On a single site there is no other site to protect and
479 + * the toggle means what it says.
480 + */
481 + if ( is_multisite() || null !== $login_security ) {
370 482 new Vigilante_Two_Factor_Email( $this->settings, $this->database, $this->activity_log, $login_security );
371 483 new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log, $login_security );
372 484 }
373 485
@@ -379,10 +491,41 @@
379 491 }
380 492
381 493 // File Integrity Scanner
382 494 if ( ! empty( $options['modules']['file_integrity'] ) ) {
383 - new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log );
495 + $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log );
496 + $file_integrity->init_hooks();
497 + $file_integrity->init_cleanup_hooks();
498 +
384 499 new Vigilante_Plugin_Status( $this->settings, $this->activity_log );
500 + } elseif ( is_admin() ) {
501 + /*
502 + * The module is off, and the cleanup goes on anyway. It is not
503 + * integrity monitoring: it takes out of the database the copy of
504 + * wp-config.php that earlier versions stored, credentials and all.
505 + * Turning the module off is not a decision to keep them.
506 + *
507 + * Two holes closed here, both reported by @calzbert after reading the
508 + * 2.11.3 diff. A site with the module off cleaned itself by neither
509 + * of its own two paths, because both hang off this class. And with
510 + * the module off on the MAIN site, the network sweep was not
511 + * registered either, which is what would have reached every other
512 + * site: the sweep removes each site's option without asking whether
513 + * the module is on over there.
514 + *
515 + * Only in the admin, because both hooks are admin_init and there is
516 + * nothing to gain from building this on a front-end request. Note
517 + * that admin-ajax.php fires admin_init too (wp-admin/admin-ajax.php
518 + * :45), so this also runs on wp_ajax_nopriv_* requests from
519 + * visitors with no session. That is deliberate and it is what the
520 + * module has been doing since 2.11.2: the cleanup asks for no
521 + * capability because it also runs under wp-cron with nobody logged
522 + * in, and all it does is take the plugin's own copy out of the
523 + * database. The network sweep, which does reach across sites, is
524 + * the one that demands manage_network_options.
525 + */
526 + $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log );
527 + $file_integrity->init_cleanup_hooks();
385 528 }
386 529
387 530 // Activity Log is always initialized (core component)
388 531 // Logging is gated by the modules.activity_log toggle and per-type flags
@@ -397,8 +540,16 @@
397 540
398 541 // Under Attack mode - always loaded (independent of modules)
399 542 new Vigilante_Under_Attack( $this->settings, $this->activity_log );
400 543
544 + // Self-protection - always loaded, NOT gated by modules.file_integrity:
545 + // the version change and watchdog paths must stay alive with the File
546 + // Integrity module off. There is no setting to gate it: see
547 + // Vigilante_Self_Integrity::is_on(). Hooks are registered here and only here;
548 + // every other new of the class is a plain object.
549 + $this->self_integrity = new Vigilante_Self_Integrity( $this->settings, $this->activity_log );
550 + $this->self_integrity->init_hooks();
551 +
401 552 // Admin interface
402 553 if ( is_admin() ) {
403 554 new Vigilante_Admin( $this->settings, $this->database, $this->activity_log );
404 555 }
@@ -465,9 +616,9 @@
465 616 * without having written a thing. The .htaccess was never refreshed after
466 617 * an update, and the one-shot snapshot behind it was consumed without
467 618 * being taken, so not even a network administrator visiting afterwards
468 619 * retried, because the version had already been marked. Reported by
469 - * calzbert, who found it reading the code.
620 + * @calzbert, who found it reading the code.
470 621 */
471 622 if ( ! Vigilante_Settings::owns_shared_files() ) {
472 623 $this->mark_server_files_synced();
473 624 return;
@@ -540,24 +691,33 @@
540 691 * state alone, so whichever request wins finishes the job and this one
541 692 * stays out of the way. Treating it as a failure would arm the one hour
542 693 * backoff for something that is already being handled.
543 694 */
544 - $locked = false;
695 + $locked = false;
696 + $incomplete = false;
697 + $unreadable = false;
698 + $settled = array( 'locked', 'block_incomplete', 'read_failed' );
545 699
546 700 if ( $needs_protection_block ) {
547 701 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php';
548 - $result = ( new Vigilante_Htaccess_Protection( $this->settings ) )->apply_rules( true );
549 - $locked = $locked || ( is_wp_error( $result ) && 'locked' === $result->get_error_code() );
550 - $failed = $failed || ( is_wp_error( $result ) && 'locked' !== $result->get_error_code() );
551 - $rewrote = true;
702 + $result = ( new Vigilante_Htaccess_Protection( $this->settings ) )->apply_rules( true );
703 + $code = is_wp_error( $result ) ? $result->get_error_code() : ( true === $result ? '' : 'unexpected_result' );
704 + $locked = $locked || 'locked' === $code;
705 + $incomplete = $incomplete || 'block_incomplete' === $code;
706 + $unreadable = $unreadable || 'read_failed' === $code;
707 + $failed = $failed || ( '' !== $code && ! in_array( $code, $settled, true ) );
708 + $rewrote = true;
552 709 }
553 710
554 711 if ( ! $locked && ! empty( $options['modules']['security_headers'] ) ) {
555 712 require_once VIGILANTE_INCLUDES_DIR . 'class-security-headers.php';
556 - $result = ( new Vigilante_Security_Headers( $this->settings ) )->apply_rules( true );
557 - $locked = $locked || ( is_wp_error( $result ) && 'locked' === $result->get_error_code() );
558 - $failed = $failed || ( is_wp_error( $result ) && 'locked' !== $result->get_error_code() );
559 - $rewrote = true;
713 + $result = ( new Vigilante_Security_Headers( $this->settings ) )->apply_rules( true );
714 + $code = is_wp_error( $result ) ? $result->get_error_code() : ( true === $result ? '' : 'unexpected_result' );
715 + $locked = $locked || 'locked' === $code;
716 + $incomplete = $incomplete || 'block_incomplete' === $code;
717 + $unreadable = $unreadable || 'read_failed' === $code;
718 + $failed = $failed || ( '' !== $code && ! in_array( $code, $settled, true ) );
719 + $rewrote = true;
560 720 }
561 721
562 722 if ( $locked ) {
563 723 return;
@@ -581,11 +741,45 @@
581 741
582 742 return;
583 743 }
584 744
745 + /*
746 + * A block with a BEGIN line and no END is not going to mend itself, so
747 + * retrying every hour would only repeat the refusal: it is recorded once
748 + * for this version, with what to do about it, and the job is marked done.
749 + * Saving the Firewall or Headers tab after fixing the file writes the
750 + * rules again.
751 + */
752 + if ( $incomplete && $this->activity_log ) {
753 + $this->activity_log->log(
754 + 'system',
755 + 'server_rules_block_incomplete',
756 + __( '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' ),
757 + array( 'version' => VIGILANTE_VERSION ),
758 + 'warning'
759 + );
760 + }
761 +
762 + /*
763 + * Same for a .htaccess that PHP can write but not read: since 2.11.8 it is
764 + * left as it is rather than replaced by the Vigilant rules alone, and
765 + * that does not mend itself either. The first version of that fix left
766 + * it to the hourly retry, with a message about read only files; found by
767 + * the cross review of 2.11.8.
768 + */
769 + if ( $unreadable && $this->activity_log ) {
770 + $this->activity_log->log(
771 + 'system',
772 + 'server_rules_read_failed',
773 + __( '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' ),
774 + array( 'version' => VIGILANTE_VERSION ),
775 + 'warning'
776 + );
777 + }
778 +
585 779 $this->mark_server_files_synced();
586 780
587 - if ( $rewrote && $this->activity_log ) {
781 + if ( $rewrote && ! $incomplete && ! $unreadable && $this->activity_log ) {
588 782 $this->activity_log->log(
589 783 'system',
590 784 'server_rules_refreshed',
591 785 sprintf(
@@ -651,10 +845,13 @@
651 845 $this->database->cleanup_expired_2fa_codes();
652 846 $this->database->cleanup_expired_trusted_devices();
653 847
654 848 // Remove sensitive files (readme.html, license.txt, licencia.txt)
655 - // WordPress core updates recreate these files, so we clean them daily
656 - $advanced = $this->settings->get_section( 'advanced' );
849 + // WordPress core updates recreate these files, so we clean them daily.
850 + // They sit in the root every site of a network shares, so only the main
851 + // site removes them, from its own settings; until 2.11.6 the daily
852 + // maintenance of any site did.
853 + $advanced = Vigilante_Settings::owns_shared_files() ? $this->settings->get_section( 'advanced' ) : array();
657 854 if ( ! empty( $advanced['remove_readme'] ) ) {
658 855 $readme_path = ABSPATH . 'readme.html';
659 856 if ( file_exists( $readme_path ) ) {
660 857 wp_delete_file( $readme_path );
@@ -669,8 +866,26 @@
669 866 }
670 867 }
671 868 }
672 869
870 + // Self-protection from cron, for sites nobody opens the admin of: the
871 + // watchdog of Vigilant's own scheduled events, and the check for a
872 + // version change made outside the WordPress updater (FTP, manual).
873 + // The version change goes first: it is the path that reports a downgrade
874 + // by email, and the watchdog's own daily check would otherwise get there
875 + // before it.
876 + if ( $this->self_integrity ) {
877 + if ( $this->self_integrity->is_enabled() ) {
878 + $this->self_integrity->detect_version_change();
879 + $this->self_integrity->run_watchdog();
880 + } else {
881 + // Switched off by code: the one line that still has to be
882 + // written, or an installation that stopped checking itself
883 + // would do it in silence.
884 + $this->self_integrity->audit_off_state();
885 + }
886 + }
887 +
673 888 // Log maintenance
674 889 $this->activity_log->log( 'system', 'maintenance', __( 'Daily maintenance completed', 'vigilante' ) );
675 890 }
676 891
@@ -780,8 +995,157 @@
780 995 $under_attack->run_analyzer_scan( 'all' );
781 996 }
782 997
783 998 /**
999 + * Verify Vigilant's own files right after the WordPress updater replaced them
1000 + *
1001 + * Runs as the OLD code with the NEW files already on disk, so the class reads
1002 + * everything from disk (the Version header, the manifest) and nothing from
1003 + * constants in memory. The first update to 3.0.0 is not seen here, because the
1004 + * code that receives the hook is 2.x: that one is covered by the 3.0.0
1005 + * migration block of Vigilante_Admin::run_migrations().
1006 + *
1007 + * Plugin_Upgrader::upgrade() passes the updated plugin in 'plugin', and
1008 + * bulk_upgrade() passes the list in 'plugins'; both are read. Replacing the
1009 + * plugin by uploading its zip, from the screen or with `wp plugin install
1010 + * --force`, goes through Plugin_Upgrader::install() instead, whose context
1011 + * names no plugin (wp-admin/includes/class-plugin-upgrader.php, install()):
1012 + * plugin_info() reads it from the folder that was written.
1013 + *
1014 + * The check itself waits for the end of the request (see
1015 + * vigilante_verify_after_upgrade()): WP_Upgrader::run() fires this hook also
1016 + * when the install failed, and restores the previous copy of the plugin on
1017 + * shutdown, so checking here reported the half-moved folder of a failed update
1018 + * as tampering, by email.
1019 + *
1020 + * @since 3.0.0
1021 + *
1022 + * @param WP_Upgrader|mixed $upgrader Upgrader instance.
1023 + * @param array $hook_extra Update context.
1024 + */
1025 +function vigilante_on_upgrader_process_complete( $upgrader, $hook_extra ) {
1026 + if ( ! is_array( $hook_extra ) || 'plugin' !== ( isset( $hook_extra['type'] ) ? $hook_extra['type'] : '' ) ) {
1027 + return;
1028 + }
1029 +
1030 + $action = isset( $hook_extra['action'] ) ? $hook_extra['action'] : '';
1031 + $files = array();
1032 + if ( 'update' === $action ) {
1033 + $files = ( ! empty( $hook_extra['plugins'] ) && is_array( $hook_extra['plugins'] ) ) ? $hook_extra['plugins'] : array();
1034 + if ( ! empty( $hook_extra['plugin'] ) ) {
1035 + $files[] = $hook_extra['plugin'];
1036 + }
1037 + } elseif ( 'install' === $action && is_object( $upgrader ) && method_exists( $upgrader, 'plugin_info' ) ) {
1038 + $installed = $upgrader->plugin_info();
1039 + if ( is_string( $installed ) && '' !== $installed ) {
1040 + $files[] = $installed;
1041 + }
1042 + } else {
1043 + return;
1044 + }
1045 + if ( ! in_array( VIGILANTE_PLUGIN_BASENAME, array_map( 'strval', $files ), true ) ) {
1046 + return;
1047 + }
1048 +
1049 + // After WP_Upgrader::restore_temp_backup() (shutdown, priority 10) and
1050 + // before WP_Upgrader::delete_temp_backup() (priority 100).
1051 + if ( false === has_action( 'shutdown', 'vigilante_verify_after_upgrade' ) ) {
1052 + add_action( 'shutdown', 'vigilante_verify_after_upgrade', 50 );
1053 + }
1054 +}
1055 +
1056 +/**
1057 + * The check after an update, at the end of the request
1058 + *
1059 + * By now a failed update has had its previous copy restored, so the files
1060 + * on disk are the ones the site will run. Hooked by
1061 + * vigilante_on_upgrader_process_complete(); a bulk update with Vigilant in
1062 + * the list hooks it once.
1063 + *
1064 + * @since 3.0.0
1065 + */
1066 +function vigilante_verify_after_upgrade() {
1067 + if ( ! class_exists( 'Vigilante_Settings' ) ) {
1068 + require_once VIGILANTE_INCLUDES_DIR . 'class-settings.php';
1069 + }
1070 + if ( ! class_exists( 'Vigilante_Self_Integrity' ) ) {
1071 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity-guidance.php';
1072 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity.php';
1073 + }
1074 +
1075 + $settings = new Vigilante_Settings();
1076 + $activity_log = null;
1077 + if ( class_exists( 'Vigilante_Activity_Log' ) && class_exists( 'Vigilante_Database' ) ) {
1078 + $database = new Vigilante_Database();
1079 + $activity_log = new Vigilante_Activity_Log( $settings, $database );
1080 + }
1081 +
1082 + $self_integrity = new Vigilante_Self_Integrity( $settings, $activity_log );
1083 + $self_integrity->handle_upgrader( vigilante_upgrader_wrote_files_on_disk() );
1084 +}
1085 +
1086 +/**
1087 + * Whether the files on disk are the ones the WordPress updater wrote in this request
1088 + *
1089 + * The marker of vigilante_mark_upgrader_wrote() says WordPress wrote the
1090 + * folder, not that the folder is still that copy at the end of the request:
1091 + * the automatic updater puts the previous copy back when the updated plugin
1092 + * breaks its loopback request, and a failed install is restored on shutdown.
1093 + * The manifest identifies the copy, so the updater is trusted only when the
1094 + * manifest on disk is the one it wrote.
1095 + *
1096 + * @since 3.0.0
1097 + *
1098 + * @return bool
1099 + */
1100 +function vigilante_upgrader_wrote_files_on_disk() {
1101 + $mark = isset( $GLOBALS['vigilante_upgrader_wrote'] ) ? $GLOBALS['vigilante_upgrader_wrote'] : null;
1102 + if ( ! is_array( $mark ) || ! array_key_exists( 'manifest', $mark ) ) {
1103 + return false;
1104 + }
1105 + return $mark['manifest'] === Vigilante_Self_Integrity::manifest_fingerprint_of( VIGILANTE_PLUGIN_DIR );
1106 +}
1107 +
1108 +/**
1109 + * Remember that the WordPress updater wrote Vigilant's folder in this request
1110 + *
1111 + * upgrader_post_install runs once per package, after the files are in place
1112 + * and before upgrader_process_complete. The hook that follows names every
1113 + * plugin of a bulk update, including the ones that were skipped, so the check
1114 + * after the update only trusts the updater when this marker says it really
1115 + * replaced the folder. Only a folder of that name in the plugins directory
1116 + * counts (a theme can have the same name), and the marker keeps the
1117 + * fingerprint of the manifest that was written, which
1118 + * vigilante_upgrader_wrote_files_on_disk() compares with the disk at the end.
1119 + *
1120 + * @since 3.0.0
1121 + *
1122 + * @param bool|WP_Error $response Installation response.
1123 + * @param array $hook_extra Extra arguments passed to hooked filters.
1124 + * @param array $result Installation result data.
1125 + * @return bool|WP_Error The response, unchanged.
1126 + */
1127 +function vigilante_mark_upgrader_wrote( $response, $hook_extra, $result ) {
1128 + if ( is_wp_error( $response ) || ! is_array( $result ) || ! isset( $result['destination_name'], $result['destination'], $result['local_destination'] ) ) {
1129 + return $response;
1130 + }
1131 + if ( dirname( VIGILANTE_PLUGIN_BASENAME ) !== $result['destination_name'] ) {
1132 + return $response;
1133 + }
1134 + if ( untrailingslashit( wp_normalize_path( (string) $result['local_destination'] ) ) !== untrailingslashit( wp_normalize_path( WP_PLUGIN_DIR ) ) ) {
1135 + return $response;
1136 + }
1137 + if ( ! class_exists( 'Vigilante_Self_Integrity' ) ) {
1138 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity-guidance.php';
1139 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity.php';
1140 + }
1141 + $GLOBALS['vigilante_upgrader_wrote'] = array(
1142 + 'manifest' => Vigilante_Self_Integrity::manifest_fingerprint_of( (string) $result['destination'] ),
1143 + );
1144 + return $response;
1145 +}
1146 +
1147 +/**
784 1148 * Plugin activation hook
785 1149 */
786 1150 function vigilante_activate() {
787 1151 require_once VIGILANTE_INCLUDES_DIR . 'class-database.php';
@@ -794,16 +1158,19 @@
794 1158 register_activation_hook( __FILE__, 'vigilante_activate' );
795 1159
796 1160 /**
797 1161 * Plugin deactivation hook
1162 + *
1163 + * @param bool $network_wide Whether core is deactivating the plugin for the whole network.
798 1164 */
799 -function vigilante_deactivate() {
1165 +function vigilante_deactivate( $network_wide = false ) {
800 1166 require_once VIGILANTE_INCLUDES_DIR . 'class-database.php';
801 1167 require_once VIGILANTE_INCLUDES_DIR . 'class-settings.php';
802 1168 require_once VIGILANTE_INCLUDES_DIR . 'class-backup-manager.php';
1169 + require_once VIGILANTE_INCLUDES_DIR . 'class-wpconfig-security.php';
803 1170 require_once VIGILANTE_INCLUDES_DIR . 'class-deactivator.php';
804 1171
805 - Vigilante_Deactivator::deactivate();
1172 + Vigilante_Deactivator::deactivate( (bool) $network_wide );
806 1173 }
807 1174 register_deactivation_hook( __FILE__, 'vigilante_deactivate' );
808 1175
809 1176 /**