| @@ -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 | /** |