| @@ -273,9 +273,29 @@ | ||
| 273 | 273 | if ( ! class_exists( 'Vigilante_File_Integrity' ) ) { |
| 274 | 274 | require_once VIGILANTE_INCLUDES_DIR . 'class-file-integrity.php'; |
| 275 | 275 | } |
| 276 | 276 | $fi = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log ); |
| 277 | - $fi->regenerate_all_baselines(); | |
| 277 | + | |
| 278 | + /* | |
| 279 | + * Only when there is nothing on record. This migration exists to | |
| 280 | + * create the baseline that did not exist, never to discard the one | |
| 281 | + * the owner approved: rebuilding it from the files takes whatever | |
| 282 | + * is on disk right now as approved, so a wp-config.php modified and | |
| 283 | + * awaiting review would be blessed in silence. | |
| 284 | + * | |
| 285 | + * And this is not theory. vigilante_db_version is written on two | |
| 286 | + * different scales into the same option: this file counts in plugin | |
| 287 | + * versions (2.11.0) and Vigilante_Database counts in schema | |
| 288 | + * versions, currently 1.4.0 (class-database.php:322 and :380). For | |
| 289 | + * version_compare, 1.4.0 is LOWER than 1.14.0, so any site whose | |
| 290 | + * option was last written by the schema runs this migration again. | |
| 291 | + * Measured on the Multisite install on 10 sep 2026: one of the three | |
| 292 | + * sites was sitting on 1.4.0. | |
| 293 | + */ | |
| 294 | + if ( ! $fi->get_critical_files_baseline() ) { | |
| 295 | + $fi->regenerate_all_baselines(); | |
| 296 | + } | |
| 297 | + | |
| 278 | 298 | update_option( 'vigilante_db_version', '1.14.0' ); |
| 279 | 299 | } |
| 280 | 300 | |
| 281 | 301 | // 2.0.0: Move hide_server_signature and remove_fingerprinting_headers |
| @@ -425,8 +445,27 @@ | ||
| 425 | 445 | } |
| 426 | 446 | |
| 427 | 447 | update_option( 'vigilante_db_version', '2.9.9' ); |
| 428 | 448 | } |
| 449 | + | |
| 450 | + /* | |
| 451 | + * 2.11.0: security release (audit of 28 Aug 2026). Runs here and not | |
| 452 | + * from Vigilante_Database::needs_update(): this option is shared with | |
| 453 | + * that class, and on any updated site it already holds a plugin version | |
| 454 | + * (2.9.9 or later), so a bump of DB_VERSION would never fire. | |
| 455 | + * create_tables() widens the email code column through dbDelta (varchar | |
| 456 | + * 6 to 64, the code is stored hashed since 2.11.0) and purge_for_2_11_0() | |
| 457 | + * does what dbDelta cannot: it empties the trusted devices, which were | |
| 458 | + * identified by User-Agent until now (S1), and the pending email codes, | |
| 459 | + * stored in clear until now (S11). Every remembered device asks for the | |
| 460 | + * second factor once more after this update, and the changelog says so. | |
| 461 | + */ | |
| 462 | + if ( version_compare( $db_version, '2.11.0', '<' ) ) { | |
| 463 | + $this->database->create_tables(); | |
| 464 | + $this->database->purge_for_2_11_0(); | |
| 465 | + | |
| 466 | + update_option( 'vigilante_db_version', '2.11.0' ); | |
| 467 | + } | |
| 429 | 468 | } |
| 430 | 469 | |
| 431 | 470 | /** |
| 432 | 471 | * Migration: Remove orphaned email fields from saved options |
| @@ -1429,8 +1468,11 @@ | ||
| 1429 | 1468 | 'currentUserId' => get_current_user_id(), |
| 1430 | 1469 | 'logoutUrl' => wp_logout_url( wp_login_url() ), |
| 1431 | 1470 | 'adminUrl' => admin_url( 'admin.php?page=vigilante' ), |
| 1432 | 1471 | 'searchIndex' => $this->get_search_index(), |
| 1472 | + // The scan repaints this table from JavaScript, so the same gate | |
| 1473 | + // has to travel with it or half the screen keeps the dead button. | |
| 1474 | + 'approvalLocked' => $this->critical_approval_locked(), | |
| 1433 | 1475 | 'underAttack' => array( |
| 1434 | 1476 | 'active' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->is_active(), |
| 1435 | 1477 | 'remaining' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->get_remaining_time(), |
| 1436 | 1478 | ), |
| @@ -1497,13 +1539,17 @@ | ||
| 1497 | 1539 | 'criticalConfigTitle' => __( 'Critical config files modified', 'vigilante' ), |
| 1498 | 1540 | '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' ), |
| 1499 | 1541 | 'approve' => __( 'Approve', 'vigilante' ), |
| 1500 | 1542 | 'approving' => __( 'Approving...', 'vigilante' ), |
| 1543 | + 'approvalLockedNotice' => $this->critical_approval_notice(), | |
| 1501 | 1544 | 'criticalApproved' => __( 'Change approved. Next scan will use the current state as baseline.', 'vigilante' ), |
| 1502 | 1545 | 'reviewChanges' => __( 'Review changes', 'vigilante' ), |
| 1503 | 1546 | 'hideChanges' => __( 'Hide changes', 'vigilante' ), |
| 1504 | 1547 | 'changes' => __( 'Changes', 'vigilante' ), |
| 1505 | 1548 | 'diffUnavailable' => __( 'Diff not available for this file (baseline was created before diff tracking was added). Approve to enable diff on future changes.', 'vigilante' ), |
| 1549 | + 'diffNetwork' => __( 'This file belongs to the whole network, so its line changes are only shown to network administrators, on the main site.', 'vigilante' ), | |
| 1550 | + 'diffRescan' => __( 'Run a new scan to see the line changes of this file.', 'vigilante' ), | |
| 1551 | + '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' ), | |
| 1506 | 1552 | 'diffEmpty' => __( 'No line-level changes detected (may be whitespace or reordering).', 'vigilante' ), |
| 1507 | 1553 | 'diffLines' => __( 'lines', 'vigilante' ), |
| 1508 | 1554 | // Under Attack mode strings |
| 1509 | 1555 | 'underAttackConfirmActivate' => __( 'Activate Under Attack mode? All visitors will see a verification page for the next 4 hours.', 'vigilante' ), |
| @@ -1766,8 +1812,21 @@ | ||
| 1766 | 1812 | </p> |
| 1767 | 1813 | <p> |
| 1768 | 1814 | <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> |
| 1769 | 1815 | </p> |
| 1816 | + <?php | |
| 1817 | + // The cache-bypass rules could not be written (a host where | |
| 1818 | + // WordPress cannot write files by itself, a held lock, a | |
| 1819 | + // failed read-back): show them, so they can be added by hand. | |
| 1820 | + $ua_instance = new Vigilante_Under_Attack( $this->settings, $this->activity_log ); | |
| 1821 | + if ( $ua_instance->cache_rules_missing() ) : | |
| 1822 | + ?> | |
| 1823 | + <p> | |
| 1824 | + <strong><?php esc_html_e( 'The cache-bypass rules could not be written to your .htaccess.', 'vigilante' ); ?></strong> | |
| 1825 | + <?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' ); ?> | |
| 1826 | + </p> | |
| 1827 | + <textarea readonly rows="9" class="large-text code" onclick="this.select();"><?php echo esc_textarea( Vigilante_Under_Attack::get_cache_bypass_block() ); ?></textarea> | |
| 1828 | + <?php endif; ?> | |
| 1770 | 1829 | </div> |
| 1771 | 1830 | <?php |
| 1772 | 1831 | } |
| 1773 | 1832 | } |
| @@ -1999,8 +2058,41 @@ | ||
| 1999 | 2058 | return ! Vigilante_Settings::can_write_shared_files(); |
| 2000 | 2059 | } |
| 2001 | 2060 | |
| 2002 | 2061 | /** |
| 2062 | + * Whether this is the main site and the user cannot change what it builds the shared files from | |
| 2063 | + * | |
| 2064 | + * See Vigilante_Settings::get_main_site_file_settings(). On a subsite those | |
| 2065 | + * settings only act on that site, so they are never locked there. | |
| 2066 | + * | |
| 2067 | + * @since 2.11.6 | |
| 2068 | + * | |
| 2069 | + * @return bool | |
| 2070 | + */ | |
| 2071 | + private function main_site_files_locked() { | |
| 2072 | + return $this->shared_files_locked() && Vigilante_Settings::owns_shared_files(); | |
| 2073 | + } | |
| 2074 | + | |
| 2075 | + /** | |
| 2076 | + * Sentence added to a bulk change when some settings were left as they were | |
| 2077 | + * | |
| 2078 | + * Importing a file, applying a preset and restoring the defaults touch every | |
| 2079 | + * section at once, so the user is told that the shared file settings did | |
| 2080 | + * not move. | |
| 2081 | + * | |
| 2082 | + * @since 2.11.6 | |
| 2083 | + * | |
| 2084 | + * @return string Empty when the user can change every setting. | |
| 2085 | + */ | |
| 2086 | + private function locked_file_settings_message() { | |
| 2087 | + if ( ! Vigilante_Settings::get_locked_file_settings() ) { | |
| 2088 | + return ''; | |
| 2089 | + } | |
| 2090 | + | |
| 2091 | + return ' ' . __( 'The settings that end up in wp-config.php or .htaccess were left as they were.', 'vigilante' ) . ' ' . Vigilante_Settings::get_shared_files_notice(); | |
| 2092 | + } | |
| 2093 | + | |
| 2094 | + /** | |
| 2003 | 2095 | * Print the shared-files notice for a section that cannot be edited here |
| 2004 | 2096 | * |
| 2005 | 2097 | * @since 2.9.8 |
| 2006 | 2098 | */ |
| @@ -2015,8 +2107,109 @@ | ||
| 2015 | 2107 | <?php |
| 2016 | 2108 | } |
| 2017 | 2109 | |
| 2018 | 2110 | /** |
| 2111 | + * Acting on another user's account needs permission over that user | |
| 2112 | + * | |
| 2113 | + * Since 2.10.3 the handlers behind these tools ask for edit_user over the | |
| 2114 | + * target, which is the rule WordPress itself applies. On a network the core | |
| 2115 | + * grants edit_user only to network administrators, so for anybody else these | |
| 2116 | + * controls do nothing. Better to say so than to paint a button that silently | |
| 2117 | + * skips every user. | |
| 2118 | + * | |
| 2119 | + * @since 2.10.4 | |
| 2120 | + * @return bool | |
| 2121 | + */ | |
| 2122 | + private function forwarded_chain_readings() { | |
| 2123 | + // Shown, not decided on: the firewall resolves the address elsewhere. | |
| 2124 | + $chain = Vigilante_IP_Utils::trusted_forwarded_for(); | |
| 2125 | + | |
| 2126 | + if ( '' === $chain ) { | |
| 2127 | + return array(); | |
| 2128 | + } | |
| 2129 | + | |
| 2130 | + $public = array(); | |
| 2131 | + | |
| 2132 | + foreach ( explode( ',', $chain ) as $entry ) { | |
| 2133 | + $address = Vigilante_IP_Utils::unmap_ipv4( trim( $entry ) ); | |
| 2134 | + | |
| 2135 | + if ( filter_var( $address, FILTER_VALIDATE_IP ) && ! Vigilante_IP_Utils::is_own_network( $address ) ) { | |
| 2136 | + $public[] = $address; | |
| 2137 | + } | |
| 2138 | + } | |
| 2139 | + | |
| 2140 | + if ( count( $public ) < 2 ) { | |
| 2141 | + return array(); | |
| 2142 | + } | |
| 2143 | + | |
| 2144 | + return array( | |
| 2145 | + 'now' => Vigilante_IP_Utils::client_from_chain( $chain ), | |
| 2146 | + 'before' => $public[0], | |
| 2147 | + ); | |
| 2148 | + } | |
| 2149 | + | |
| 2150 | + /** | |
| 2151 | + * Whether the user tools of this screen are out of reach for this user | |
| 2152 | + * | |
| 2153 | + * @return bool | |
| 2154 | + */ | |
| 2155 | + private function user_actions_locked() { | |
| 2156 | + // On a single site edit_user maps to edit_users, which a custom role with | |
| 2157 | + // manage_options may lack: since 2.11.8 approving and rejecting a pending | |
| 2158 | + // registration ask for it, so the buttons have to say so there too. | |
| 2159 | + return is_multisite() ? ! current_user_can( 'manage_network_users' ) : ! current_user_can( 'edit_users' ); | |
| 2160 | + } | |
| 2161 | + | |
| 2162 | + /** | |
| 2163 | + * Print the notice for user tools that cannot be used from this site | |
| 2164 | + * | |
| 2165 | + * @since 2.10.4 | |
| 2166 | + */ | |
| 2167 | + private function render_user_actions_notice() { | |
| 2168 | + if ( ! $this->user_actions_locked() ) { | |
| 2169 | + return; | |
| 2170 | + } | |
| 2171 | + ?> | |
| 2172 | + <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;"> | |
| 2173 | + <?php if ( is_multisite() ) : ?> | |
| 2174 | + <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> | |
| 2175 | + <?php else : ?> | |
| 2176 | + <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> | |
| 2177 | + <?php endif; ?> | |
| 2178 | + </div> | |
| 2179 | + <?php | |
| 2180 | + } | |
| 2181 | + | |
| 2182 | + /** | |
| 2183 | + * Approving a change to the shared config files needs the network | |
| 2184 | + * | |
| 2185 | + * Since 2.11.3 the handler behind the Approve button asks for | |
| 2186 | + * manage_network_options, because the two files it approves, wp-config.php | |
| 2187 | + * and the root .htaccess, belong to the installation, and so does the | |
| 2188 | + * record of them. The button, though, went on being painted for everybody, | |
| 2189 | + * so the administrator of a subsite saw the warning, saw the button, | |
| 2190 | + * pressed it and got "Permission denied" with no explanation. That is | |
| 2191 | + * exactly what user_actions_locked() above exists to avoid, one release | |
| 2192 | + * later and one screen over. Flagged by @calzbert. | |
| 2193 | + * | |
| 2194 | + * @since 2.11.4 | |
| 2195 | + * @return bool | |
| 2196 | + */ | |
| 2197 | + private function critical_approval_locked() { | |
| 2198 | + return is_multisite() && ! current_user_can( 'manage_network_options' ); | |
| 2199 | + } | |
| 2200 | + | |
| 2201 | + /** | |
| 2202 | + * The line that replaces the Approve button where it cannot be used | |
| 2203 | + * | |
| 2204 | + * @since 2.11.4 | |
| 2205 | + * @return string | |
| 2206 | + */ | |
| 2207 | + private function critical_approval_notice() { | |
| 2208 | + 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' ); | |
| 2209 | + } | |
| 2210 | + | |
| 2211 | + /** | |
| 2019 | 2212 | * Check if module is disabled and render warning |
| 2020 | 2213 | * |
| 2021 | 2214 | * @param string $module_key Module key. |
| 2022 | 2215 | * @return bool True if disabled. |
| @@ -2560,14 +2753,15 @@ | ||
| 2560 | 2753 | |
| 2561 | 2754 | <?php $this->render_analyzer_widget( $analyzer_last_scan, $analyzer_history, $analyzer_categories_def, $analyzer_settings ); ?> |
| 2562 | 2755 | |
| 2563 | 2756 | <div class="vigilante-modules-grid"> |
| 2564 | - <h2><?php esc_html_e( 'Security Modules', 'vigilante' ); ?></h2> | |
| 2757 | + <h2 id="vigilante-section-dashboard-modules"><?php esc_html_e( 'Security Modules', 'vigilante' ); ?></h2> | |
| 2565 | 2758 | <p class="description"><?php esc_html_e( 'Enable or disable security modules. Each module controls a tab with detailed settings.', 'vigilante' ); ?></p> |
| 2566 | 2759 | <div class="vigilante-modules-list"> |
| 2567 | 2760 | <?php foreach ( $options['modules'] as $module => $enabled ) : |
| 2568 | 2761 | $label = isset( $module_labels[ $module ] ) ? $module_labels[ $module ] : ucwords( str_replace( '_', ' ', $module ) ); |
| 2569 | 2762 | $description = isset( $module_descriptions[ $module ] ) ? $module_descriptions[ $module ] : ''; |
| 2763 | + $vg_module_locked = $this->main_site_files_locked() && in_array( $module, Vigilante_Settings::get_main_site_file_settings()['modules'], true ); | |
| 2570 | 2764 | ?> |
| 2571 | 2765 | <div class="vigilante-module-item <?php echo $enabled ? 'enabled' : 'disabled'; ?>"> |
| 2572 | 2766 | <div class="vigilante-module-header"> |
| 2573 | 2767 | <span class="vigilante-module-status"></span> |
| @@ -2580,8 +2774,9 @@ | ||
| 2580 | 2774 | <input type="checkbox" |
| 2581 | 2775 | name="modules[<?php echo esc_attr( $module ); ?>]" |
| 2582 | 2776 | value="1" |
| 2583 | 2777 | <?php checked( $enabled ); ?> |
| 2778 | + <?php disabled( $vg_module_locked ); ?> | |
| 2584 | 2779 | aria-label="<?php echo esc_attr( $toggle_label ); ?>" |
| 2585 | 2780 | data-module="<?php echo esc_attr( $module ); ?>"> |
| 2586 | 2781 | <span class="vigilante-toggle-slider"></span> |
| 2587 | 2782 | </label> |
| @@ -2588,8 +2783,11 @@ | ||
| 2588 | 2783 | </div> |
| 2589 | 2784 | <?php if ( $description ) : ?> |
| 2590 | 2785 | <p class="vigilante-module-desc"><?php echo esc_html( $description ); ?></p> |
| 2591 | 2786 | <?php endif; ?> |
| 2787 | + <?php if ( $vg_module_locked ) : ?> | |
| 2788 | + <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> | |
| 2789 | + <?php endif; ?> | |
| 2592 | 2790 | </div> |
| 2593 | 2791 | <?php endforeach; ?> |
| 2594 | 2792 | </div> |
| 2595 | 2793 | </div> |
| @@ -2621,9 +2819,9 @@ | ||
| 2621 | 2819 | $ua_remaining_hours = floor( $ua_remaining / 3600 ); |
| 2622 | 2820 | $ua_remaining_mins = floor( ( $ua_remaining % 3600 ) / 60 ); |
| 2623 | 2821 | ?> |
| 2624 | 2822 | <div class="vigilante-preset-card vigilante-under-attack-card <?php echo $ua_active ? 'vigilante-under-attack-active' : ''; ?>"> |
| 2625 | - <h3> | |
| 2823 | + <h3 id="vigilante-section-dashboard-under-attack"> | |
| 2626 | 2824 | <span class="dashicons dashicons-shield"></span> |
| 2627 | 2825 | <?php esc_html_e( 'Under Attack', 'vigilante' ); ?> |
| 2628 | 2826 | </h3> |
| 2629 | 2827 | <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> |
| @@ -2979,14 +3177,21 @@ | ||
| 2979 | 3177 | <?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' ); ?> |
| 2980 | 3178 | </p> |
| 2981 | 3179 | </div> |
| 2982 | 3180 | |
| 3181 | + <?php $vg_main_locked = $this->main_site_files_locked(); ?> | |
| 3182 | + <?php if ( $vg_main_locked ) : ?> | |
| 3183 | + <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;"> | |
| 3184 | + <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> | |
| 3185 | + </div> | |
| 3186 | + <?php endif; ?> | |
| 3187 | + | |
| 2983 | 3188 | <table class="form-table"> |
| 2984 | 3189 | <tr> |
| 2985 | 3190 | <th scope="row"><?php esc_html_e( 'Block Bad Query Strings', 'vigilante' ); ?></th> |
| 2986 | 3191 | <td> |
| 2987 | 3192 | <label> |
| 2988 | - <input type="checkbox" name="firewall[block_bad_query_strings]" value="1" <?php checked( ! empty( $options['block_bad_query_strings'] ) ); ?>> | |
| 3193 | + <input type="checkbox" name="firewall[block_bad_query_strings]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( ! empty( $options['block_bad_query_strings'] ) ); ?>> | |
| 2989 | 3194 | <?php esc_html_e( 'Block malicious query string patterns', 'vigilante' ); ?> |
| 2990 | 3195 | </label> |
| 2991 | 3196 | </td> |
| 2992 | 3197 | </tr> |
| @@ -3029,9 +3234,9 @@ | ||
| 3029 | 3234 | <tr> |
| 3030 | 3235 | <th scope="row"><?php esc_html_e( 'Block Bad Bots', 'vigilante' ); ?></th> |
| 3031 | 3236 | <td> |
| 3032 | 3237 | <label> |
| 3033 | - <input type="checkbox" name="firewall[block_bad_bots]" value="1" <?php checked( ! empty( $options['block_bad_bots'] ) ); ?>> | |
| 3238 | + <input type="checkbox" name="firewall[block_bad_bots]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( ! empty( $options['block_bad_bots'] ) ); ?>> | |
| 3034 | 3239 | <?php esc_html_e( 'Block known malicious bots and scanners', 'vigilante' ); ?> |
| 3035 | 3240 | </label> |
| 3036 | 3241 | </td> |
| 3037 | 3242 | </tr> |
| @@ -3145,8 +3350,29 @@ | ||
| 3145 | 3350 | </table> |
| 3146 | 3351 | </div> |
| 3147 | 3352 | <?php endif; ?> |
| 3148 | 3353 | |
| 3354 | + <?php | |
| 3355 | + // Since 2.11.8 X-Forwarded-For is read from its end, where the proxy | |
| 3356 | + // writes. The administrator's own request shows whether that end is | |
| 3357 | + // a CDN or a balancer for everybody here. Cross review of 2.11.8. | |
| 3358 | + $xff_readings = $this->forwarded_chain_readings(); | |
| 3359 | + if ( $xff_readings ) : | |
| 3360 | + ?> | |
| 3361 | + <div id="vigilante-xff-chain-notice" class="notice notice-warning inline" style="margin:10px 0 16px;padding:8px 12px;"> | |
| 3362 | + <p style="margin:0;"> | |
| 3363 | + <?php | |
| 3364 | + printf( | |
| 3365 | + /* translators: 1: address Vigilant reads now, 2: address earlier versions read */ | |
| 3366 | + 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; up to version 2.11.7 it read 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' ), | |
| 3367 | + esc_html( $xff_readings['now'] ), | |
| 3368 | + esc_html( $xff_readings['before'] ) | |
| 3369 | + ); | |
| 3370 | + ?> | |
| 3371 | + </p> | |
| 3372 | + </div> | |
| 3373 | + <?php endif; ?> | |
| 3374 | + | |
| 3149 | 3375 | <h3><?php esc_html_e( 'IP Lists', 'vigilante' ); ?></h3> |
| 3150 | 3376 | <p class="description"> |
| 3151 | 3377 | <?php |
| 3152 | 3378 | printf( |
| @@ -3160,9 +3386,9 @@ | ||
| 3160 | 3386 | <tr> |
| 3161 | 3387 | <th scope="row"><label for="vigilante-f-firewall-trusted-proxy-header"><?php esc_html_e( 'Visitor IP detection', 'vigilante' ); ?></label></th> |
| 3162 | 3388 | <td> |
| 3163 | 3389 | <?php $proxy_header = $options['trusted_proxy_header'] ?? ''; ?> |
| 3164 | - <select id="vigilante-f-firewall-trusted-proxy-header" name="firewall[trusted_proxy_header]"> | |
| 3390 | + <select id="vigilante-f-firewall-trusted-proxy-header" name="firewall[trusted_proxy_header]" <?php disabled( $vg_main_locked ); ?>> | |
| 3165 | 3391 | <option value="" <?php selected( $proxy_header, '' ); ?>><?php esc_html_e( 'Direct connection, only REMOTE_ADDR (recommended)', 'vigilante' ); ?></option> |
| 3166 | 3392 | <option value="cf-connecting-ip" <?php selected( $proxy_header, 'cf-connecting-ip' ); ?>><?php esc_html_e( 'Behind Cloudflare (CF-Connecting-IP)', 'vigilante' ); ?></option> |
| 3167 | 3393 | <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> |
| 3168 | 3394 | <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> |
| @@ -3174,9 +3400,9 @@ | ||
| 3174 | 3400 | </tr> |
| 3175 | 3401 | <tr> |
| 3176 | 3402 | <th scope="row"><label for="vigilante-f-firewall-ip-whitelist"><?php esc_html_e( 'IP Whitelist', 'vigilante' ); ?></label></th> |
| 3177 | 3403 | <td> |
| 3178 | - <textarea id="vigilante-f-firewall-ip-whitelist" name="firewall[ip_whitelist]" rows="4" class="large-text code" placeholder="192.168.1.50 192.168.1.0/24 192.168.1.*"><?php echo esc_textarea( implode( "\n", $options['ip_whitelist'] ?? array() ) ); ?></textarea> | |
| 3404 | + <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 192.168.1.0/24 192.168.1.*"><?php echo esc_textarea( implode( "\n", $options['ip_whitelist'] ?? array() ) ); ?></textarea> | |
| 3179 | 3405 | <p class="description"> |
| 3180 | 3406 | <?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' ); ?> |
| 3181 | 3407 | <br> |
| 3182 | 3408 | <?php |
| @@ -3215,9 +3441,9 @@ | ||
| 3215 | 3441 | <table class="form-table"> |
| 3216 | 3442 | <tr> |
| 3217 | 3443 | <th scope="row"><label for="vigilante-f-firewall-ua-whitelist"><?php esc_html_e( 'User-Agent Whitelist', 'vigilante' ); ?></label></th> |
| 3218 | 3444 | <td> |
| 3219 | - <textarea id="vigilante-f-firewall-ua-whitelist" name="firewall[ua_whitelist]" rows="4" class="large-text code"><?php echo esc_textarea( implode( "\n", $options['ua_whitelist'] ?? array() ) ); ?></textarea> | |
| 3445 | + <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> | |
| 3220 | 3446 | <p class="description"><?php esc_html_e( 'One User-Agent per line. These will bypass all firewall checks. Example: ManageWP, MainWP, UptimeRobot.', 'vigilante' ); ?></p> |
| 3221 | 3447 | </td> |
| 3222 | 3448 | </tr> |
| 3223 | 3449 | <tr> |
| @@ -3505,9 +3731,9 @@ | ||
| 3505 | 3731 | $two_factor = $options['two_factor'] ?? array(); |
| 3506 | 3732 | $two_factor_enabled = ! empty( $two_factor['enabled'] ); |
| 3507 | 3733 | ?> |
| 3508 | 3734 | <div class="vigilante-settings-section vigilante-lockout-section"> |
| 3509 | - <h2><?php esc_html_e( 'Login Protection Status', 'vigilante' ); ?></h2> | |
| 3735 | + <h2 id="vigilante-section-login-status"><?php esc_html_e( 'Login Protection Status', 'vigilante' ); ?></h2> | |
| 3510 | 3736 | |
| 3511 | 3737 | <table class="form-table"> |
| 3512 | 3738 | <tr> |
| 3513 | 3739 | <th scope="row"><?php esc_html_e( 'Current settings', 'vigilante' ); ?></th> |
| @@ -3667,9 +3893,9 @@ | ||
| 3667 | 3893 | $excluded = $two_factor['excluded_users'] ?? array(); |
| 3668 | 3894 | $method = $two_factor['method'] ?? 'email'; |
| 3669 | 3895 | $grace_days = $two_factor['grace_period_days'] ?? 3; |
| 3670 | 3896 | ?> |
| 3671 | - <h3> | |
| 3897 | + <h3 id="vigilante-section-login-2fa"> | |
| 3672 | 3898 | <?php esc_html_e( 'Two-Factor Authentication (2FA)', 'vigilante' ); ?> |
| 3673 | 3899 | <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 3674 | 3900 | <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span> |
| 3675 | 3901 | </h3> |
| @@ -4040,9 +4266,9 @@ | ||
| 4040 | 4266 | </td> |
| 4041 | 4267 | </tr> |
| 4042 | 4268 | </table> |
| 4043 | 4269 | |
| 4044 | - <h3><?php esc_html_e( 'Content Security Policy', 'vigilante' ); ?></h3> | |
| 4270 | + <h3 id="vigilante-section-headers-csp"><?php esc_html_e( 'Content Security Policy', 'vigilante' ); ?></h3> | |
| 4045 | 4271 | <table class="form-table"> |
| 4046 | 4272 | <tr> |
| 4047 | 4273 | <th scope="row"><?php esc_html_e( 'Enable CSP', 'vigilante' ); ?></th> |
| 4048 | 4274 | <td> |
| @@ -4062,9 +4288,9 @@ | ||
| 4062 | 4288 | </td> |
| 4063 | 4289 | </tr> |
| 4064 | 4290 | </table> |
| 4065 | 4291 | |
| 4066 | - <h3><?php esc_html_e( 'HTTPS', 'vigilante' ); ?></h3> | |
| 4292 | + <h3 id="vigilante-section-headers-force-https"><?php esc_html_e( 'HTTPS', 'vigilante' ); ?></h3> | |
| 4067 | 4293 | <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> |
| 4068 | 4294 | <table class="form-table"> |
| 4069 | 4295 | <tr> |
| 4070 | 4296 | <th scope="row"><?php esc_html_e( 'Redirect HTTP to HTTPS', 'vigilante' ); ?></th> |
| @@ -4107,9 +4333,9 @@ | ||
| 4107 | 4333 | </td> |
| 4108 | 4334 | </tr> |
| 4109 | 4335 | </table> |
| 4110 | 4336 | |
| 4111 | - <h3><?php esc_html_e( 'HSTS (HTTP Strict Transport Security)', 'vigilante' ); ?></h3> | |
| 4337 | + <h3 id="vigilante-section-headers-hsts"><?php esc_html_e( 'HSTS (HTTP Strict Transport Security)', 'vigilante' ); ?></h3> | |
| 4112 | 4338 | <?php $vig_home_https = ( 0 === strpos( (string) get_option( 'home' ), 'https://' ) ); ?> |
| 4113 | 4339 | <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> |
| 4114 | 4340 | <?php if ( ! $vig_home_https ) : ?> |
| 4115 | 4341 | <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> |
| @@ -4150,9 +4376,9 @@ | ||
| 4150 | 4376 | </td> |
| 4151 | 4377 | </tr> |
| 4152 | 4378 | </table> |
| 4153 | 4379 | |
| 4154 | - <h3><?php esc_html_e( 'Server Identity', 'vigilante' ); ?></h3> | |
| 4380 | + <h3 id="vigilante-section-headers-fingerprint"><?php esc_html_e( 'Server Identity', 'vigilante' ); ?></h3> | |
| 4155 | 4381 | <p class="description"><?php esc_html_e( 'Hide identifying information that servers expose in responses.', 'vigilante' ); ?></p> |
| 4156 | 4382 | <table class="form-table"> |
| 4157 | 4383 | <tr> |
| 4158 | 4384 | <th scope="row"><?php esc_html_e( 'Server Signature', 'vigilante' ); ?></th> |
| @@ -4741,8 +4967,11 @@ | ||
| 4741 | 4967 | <h2 class="vigilante-tools-header"> |
| 4742 | 4968 | <?php esc_html_e( 'User security tools', 'vigilante' ); ?> |
| 4743 | 4969 | </h2> |
| 4744 | 4970 | |
| 4971 | + <?php $this->render_user_actions_notice(); ?> | |
| 4972 | + <?php if ( ! $this->user_actions_locked() ) : ?> | |
| 4973 | + | |
| 4745 | 4974 | <!-- Force Password Reset --> |
| 4746 | 4975 | <div class="vigilante-tool-box"> |
| 4747 | 4976 | <h3><?php esc_html_e( 'Force password reset', 'vigilante' ); ?></h3> |
| 4748 | 4977 | <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> |
| @@ -4878,9 +5107,9 @@ | ||
| 4878 | 5107 | <?php |
| 4879 | 5108 | $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log ); |
| 4880 | 5109 | $pending_users = $user_security->get_pending_users(); |
| 4881 | 5110 | ?> |
| 4882 | - <div class="vigilante-tool-box vigilante-pending-users-section"> | |
| 5111 | + <div id="vigilante-section-users-pending" class="vigilante-tool-box vigilante-pending-users-section"> | |
| 4883 | 5112 | <h3> |
| 4884 | 5113 | <?php esc_html_e( 'Pending registrations', 'vigilante' ); ?> |
| 4885 | 5114 | <?php if ( count( $pending_users ) > 0 ) : ?> |
| 4886 | 5115 | <span class="vigilante-badge vigilante-badge-warning"><?php echo esc_html( count( $pending_users ) ); ?></span> |
| @@ -4897,8 +5126,9 @@ | ||
| 4897 | 5126 | <span class="dashicons dashicons-yes-alt"></span> |
| 4898 | 5127 | <p><?php esc_html_e( 'No pending registrations.', 'vigilante' ); ?></p> |
| 4899 | 5128 | </div> |
| 4900 | 5129 | <?php else : ?> |
| 5130 | + <?php $this->render_user_actions_notice(); ?> | |
| 4901 | 5131 | <table class="wp-list-table widefat fixed striped vigilante-pending-users-table"> |
| 4902 | 5132 | <thead> |
| 4903 | 5133 | <tr> |
| 4904 | 5134 | <th><?php esc_html_e( 'User', 'vigilante' ); ?></th> |
| @@ -4927,12 +5157,12 @@ | ||
| 4927 | 5157 | } |
| 4928 | 5158 | ?> |
| 4929 | 5159 | </td> |
| 4930 | 5160 | <td> |
| 4931 | - <button type="button" class="button button-small vigilante-approve-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>"> | |
| 5161 | + <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() ); ?>> | |
| 4932 | 5162 | <?php esc_html_e( 'Approve', 'vigilante' ); ?> |
| 4933 | 5163 | </button> |
| 4934 | - <button type="button" class="button button-small vigilante-reject-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>" style="color: #d63638;"> | |
| 5164 | + <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() ); ?>> | |
| 4935 | 5165 | <?php esc_html_e( 'Reject', 'vigilante' ); ?> |
| 4936 | 5166 | </button> |
| 4937 | 5167 | </td> |
| 4938 | 5168 | </tr> |
| @@ -5054,8 +5284,10 @@ | ||
| 5054 | 5284 | </button> |
| 5055 | 5285 | </p> |
| 5056 | 5286 | </div> |
| 5057 | 5287 | </div> |
| 5288 | + | |
| 5289 | + <?php endif; ?> | |
| 5058 | 5290 | </div> |
| 5059 | 5291 | <?php |
| 5060 | 5292 | } |
| 5061 | 5293 | |
| @@ -5824,8 +6056,13 @@ | ||
| 5824 | 6056 | */ |
| 5825 | 6057 | private function render_tab_file_integrity() { |
| 5826 | 6058 | $is_disabled = $this->render_module_disabled_notice( 'file_integrity' ); |
| 5827 | 6059 | $options = $this->settings->get_section( 'file_integrity' ); |
| 6060 | + // On the main site of a network the critical-file scan is the network's | |
| 6061 | + // canary for a change to wp-config.php or the root .htaccess, so a | |
| 6062 | + // main-site admin without network rights cannot turn it off. Since | |
| 6063 | + // 2.11.8; see Vigilante_Settings::get_main_site_file_settings(). | |
| 6064 | + $vg_main_locked = $this->main_site_files_locked(); | |
| 5828 | 6065 | $last_scan = get_option( 'vigilante_last_integrity_scan' ); |
| 5829 | 6066 | $last_results = get_option( 'vigilante_last_integrity_results' ); |
| 5830 | 6067 | $ignored_files = get_option( 'vigilante_ignored_files', array() ); |
| 5831 | 6068 | |
| @@ -5951,10 +6188,13 @@ | ||
| 5951 | 6188 | <?php esc_html_e( 'Uploads directory (detect PHP files, double extensions, .htaccess)', 'vigilante' ); ?> |
| 5952 | 6189 | </label> |
| 5953 | 6190 | <br> |
| 5954 | 6191 | <label> |
| 5955 | - <input type="checkbox" name="file_integrity[scan_critical_config]" value="1" <?php checked( $options['scan_critical_config'] ?? true ); ?>> | |
| 6192 | + <input type="checkbox" name="file_integrity[scan_critical_config]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( $options['scan_critical_config'] ?? true ); ?>> | |
| 5956 | 6193 | <?php esc_html_e( 'Critical config files (wp-config.php, .htaccess baseline monitoring)', 'vigilante' ); ?> |
| 6194 | + <?php if ( $vg_main_locked ) : ?> | |
| 6195 | + <span class="description" style="display:block;margin-left:24px;"><?php echo esc_html( Vigilante_Settings::get_shared_files_notice() ); ?></span> | |
| 6196 | + <?php endif; ?> | |
| 5957 | 6197 | </label> |
| 5958 | 6198 | <br> |
| 5959 | 6199 | <label> |
| 5960 | 6200 | <input type="checkbox" name="file_integrity[check_closed_plugins]" value="1" <?php checked( $options['check_closed_plugins'] ?? true ); ?>> |
| @@ -6233,9 +6473,15 @@ | ||
| 6233 | 6473 | $crit_diff = $crit_item['diff'] ?? array(); |
| 6234 | 6474 | $crit_id = sanitize_html_class( $crit_file ); |
| 6235 | 6475 | $added_count = is_array( $crit_diff ) ? count( $crit_diff['added'] ?? array() ) : 0; |
| 6236 | 6476 | $removed_count = is_array( $crit_diff ) ? count( $crit_diff['removed'] ?? array() ) : 0; |
| 6237 | - $diff_unavailable = is_array( $crit_diff ) && ! empty( $crit_diff['unavailable'] ); | |
| 6477 | + // The lines of a shared file are for whoever approves it. Results | |
| 6478 | + // stored before 2.11.8 on the main site still carry them, so the | |
| 6479 | + // screen asks too, not only the scan that wrote them. | |
| 6480 | + $diff_network = ( is_array( $crit_diff ) && ! empty( $crit_diff['network'] ) ) || $this->critical_approval_locked(); | |
| 6481 | + $diff_rescan = is_array( $crit_diff ) && ! empty( $crit_diff['rescan'] ); | |
| 6482 | + $diff_redaction = is_array( $crit_diff ) && ! empty( $crit_diff['redaction'] ); | |
| 6483 | + $diff_unavailable = $diff_network || ( is_array( $crit_diff ) && ! empty( $crit_diff['unavailable'] ) ); | |
| 6238 | 6484 | ?> |
| 6239 | 6485 | <tr> |
| 6240 | 6486 | <td><code style="color: #e36210;"><?php echo esc_html( $crit_file ); ?></code></td> |
| 6241 | 6487 | <td> |
| @@ -6258,18 +6504,36 @@ | ||
| 6258 | 6504 | <td> |
| 6259 | 6505 | <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' ); ?>"> |
| 6260 | 6506 | <?php esc_html_e( 'Review changes', 'vigilante' ); ?> |
| 6261 | 6507 | </button> |
| 6262 | - <button type="button" class="button button-small button-primary vigilante-approve-critical-file" data-file="<?php echo esc_attr( $crit_file ); ?>"> | |
| 6263 | - <?php esc_html_e( 'Approve', 'vigilante' ); ?> | |
| 6264 | - </button> | |
| 6508 | + <?php if ( $this->critical_approval_locked() ) : ?> | |
| 6509 | + <span class="description" style="display:block;margin-top:4px;"> | |
| 6510 | + <?php echo esc_html( $this->critical_approval_notice() ); ?> | |
| 6511 | + </span> | |
| 6512 | + <?php else : ?> | |
| 6513 | + <button type="button" class="button button-small button-primary vigilante-approve-critical-file" data-file="<?php echo esc_attr( $crit_file ); ?>"> | |
| 6514 | + <?php esc_html_e( 'Approve', 'vigilante' ); ?> | |
| 6515 | + </button> | |
| 6516 | + <?php endif; ?> | |
| 6265 | 6517 | </td> |
| 6266 | 6518 | </tr> |
| 6267 | 6519 | <tr id="vigilante-critical-content-<?php echo esc_attr( $crit_id ); ?>" class="vigilante-critical-content-row" style="display:none;"> |
| 6268 | 6520 | <td colspan="3" style="padding: 0;"> |
| 6269 | 6521 | <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;"> |
| 6270 | - <?php if ( $diff_unavailable ) : ?> | |
| 6522 | + <?php if ( $diff_network ) : ?> | |
| 6271 | 6523 | <p style="color: #50575e; font-style: italic; margin: 0;"> |
| 6524 | + <?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' ); ?> | |
| 6525 | + </p> | |
| 6526 | + <?php elseif ( $diff_rescan ) : ?> | |
| 6527 | + <p style="color: #50575e; font-style: italic; margin: 0;"> | |
| 6528 | + <?php esc_html_e( 'Run a new scan to see the line changes of this file.', 'vigilante' ); ?> | |
| 6529 | + </p> | |
| 6530 | + <?php elseif ( $diff_redaction ) : ?> | |
| 6531 | + <p style="color: #50575e; font-style: italic; margin: 0;"> | |
| 6532 | + <?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' ); ?> | |
| 6533 | + </p> | |
| 6534 | + <?php elseif ( $diff_unavailable ) : ?> | |
| 6535 | + <p style="color: #50575e; font-style: italic; margin: 0;"> | |
| 6272 | 6536 | <?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' ); ?> |
| 6273 | 6537 | </p> |
| 6274 | 6538 | <?php elseif ( empty( $crit_diff['added'] ) && empty( $crit_diff['removed'] ) ) : ?> |
| 6275 | 6539 | <p style="color: #50575e; font-style: italic; margin: 0;"> |
| @@ -6297,9 +6561,9 @@ | ||
| 6297 | 6561 | <?php endif; ?> |
| 6298 | 6562 | |
| 6299 | 6563 | <?php if ( $has_closed ) : ?> |
| 6300 | 6564 | <div class="vigilante-file-list vigilante-closed-plugins"> |
| 6301 | - <h3 style="color: #d63638;"><?php esc_html_e( 'Closed + Removed Plugins', 'vigilante' ); ?></h3> | |
| 6565 | + <h3 id="vigilante-section-fi-closed-plugins" style="color: #d63638;"><?php esc_html_e( 'Closed + Removed Plugins', 'vigilante' ); ?></h3> | |
| 6302 | 6566 | <p class="description" style="color: #d63638;"> |
| 6303 | 6567 | <?php esc_html_e( '⚠ 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' ); ?> |
| 6304 | 6568 | </p> |
| 6305 | 6569 | <table class="wp-list-table widefat striped"> |
| @@ -6508,8 +6772,15 @@ | ||
| 6508 | 6772 | if ( ! current_user_can( 'manage_options' ) ) { |
| 6509 | 6773 | wp_die( esc_html__( 'Permission denied.', 'vigilante' ), 403 ); |
| 6510 | 6774 | } |
| 6511 | 6775 | |
| 6776 | + // The archive carries wp-config.php, which a whole network shares. On a | |
| 6777 | + // network manage_options is held by every subsite administrator, so the | |
| 6778 | + // same gate the writers use applies here. | |
| 6779 | + if ( ! Vigilante_Settings::can_write_shared_files() ) { | |
| 6780 | + wp_die( esc_html( Vigilante_Settings::get_shared_files_notice() ), 403 ); | |
| 6781 | + } | |
| 6782 | + | |
| 6512 | 6783 | $backup_manager = new Vigilante_Backup_Manager(); |
| 6513 | 6784 | $result = $backup_manager->stream_files_zip(); |
| 6514 | 6785 | |
| 6515 | 6786 | // stream_files_zip() exits on success; only a WP_Error returns here. |
| @@ -6598,8 +6869,27 @@ | ||
| 6598 | 6869 | |
| 6599 | 6870 | // Read ONLY saved options from database (not merged with defaults) |
| 6600 | 6871 | $saved_options = get_option( Vigilante_Settings::OPTION_NAME, array() ); |
| 6601 | 6872 | |
| 6873 | + // What is stored before this request changes anything: the shared file | |
| 6874 | + // settings this user may not change are put back from here (2.11.6). | |
| 6875 | + $stored_options = $saved_options; | |
| 6876 | + $locked = Vigilante_Settings::get_locked_file_settings(); | |
| 6877 | + | |
| 6878 | + if ( isset( $locked[ $section ] ) && true === $locked[ $section ] ) { | |
| 6879 | + wp_send_json_error( Vigilante_Settings::get_shared_files_notice() ); | |
| 6880 | + } | |
| 6881 | + | |
| 6882 | + // A module switch is a single key, so refusing says more than a success | |
| 6883 | + // that changed nothing, and the dashboard puts the toggle back. | |
| 6884 | + if ( 'modules' === $section && isset( $locked['modules'], $data['modules'] ) && is_array( $locked['modules'] ) && is_array( $data['modules'] ) ) { | |
| 6885 | + foreach ( array_keys( $data['modules'] ) as $vg_module ) { | |
| 6886 | + if ( in_array( sanitize_key( $vg_module ), $locked['modules'], true ) ) { | |
| 6887 | + wp_send_json_error( Vigilante_Settings::get_shared_files_notice() ); | |
| 6888 | + } | |
| 6889 | + } | |
| 6890 | + } | |
| 6891 | + | |
| 6602 | 6892 | $rejected_ips = array(); |
| 6603 | 6893 | |
| 6604 | 6894 | // Handle modules |
| 6605 | 6895 | if ( 'modules' === $section && isset( $data['modules'] ) ) { |
| @@ -6638,8 +6928,10 @@ | ||
| 6638 | 6928 | |
| 6639 | 6929 | // Clear cache before saving |
| 6640 | 6930 | wp_cache_delete( Vigilante_Settings::OPTION_NAME, 'options' ); |
| 6641 | 6931 | |
| 6932 | + $saved_options = Vigilante_Settings::keep_locked_file_settings( $saved_options, $stored_options ); | |
| 6933 | + | |
| 6642 | 6934 | // Save to database |
| 6643 | 6935 | update_option( Vigilante_Settings::OPTION_NAME, $saved_options ); |
| 6644 | 6936 | |
| 6645 | 6937 | // Clear the settings cache |
| @@ -7062,13 +7354,27 @@ | ||
| 7062 | 7354 | |
| 7063 | 7355 | // Sanitize imported data recursively |
| 7064 | 7356 | $imported = map_deep( $imported, 'sanitize_text_field' ); |
| 7065 | 7357 | |
| 7066 | - // Validate structure | |
| 7067 | - $defaults = $this->settings->get_default_options(); | |
| 7068 | - $merged = array_replace_recursive( $defaults, $imported ); | |
| 7358 | + // Validate structure: only sections and keys of the schema survive, and | |
| 7359 | + // every value takes the type of its default. Until 2.11.0 this was an | |
| 7360 | + // array_replace_recursive() of the file over the defaults, so any key in | |
| 7361 | + // the file, known or not, landed in vigilante_options (S7). Sections | |
| 7362 | + // the file does not carry keep their defaults; a section it does carry | |
| 7363 | + // replaces the default one whole, because validate_options() has | |
| 7364 | + // already filled in whatever the file left out. | |
| 7365 | + $defaults = $this->settings->get_default_options(); | |
| 7366 | + $validated = $this->settings->validate_options( $imported ); | |
| 7367 | + $merged = $defaults; | |
| 7069 | 7368 | |
| 7369 | + foreach ( $validated as $section => $data ) { | |
| 7370 | + if ( is_array( $data ) ) { | |
| 7371 | + $merged[ $section ] = $data; | |
| 7372 | + } | |
| 7373 | + } | |
| 7374 | + | |
| 7070 | 7375 | // Save |
| 7376 | + $merged = Vigilante_Settings::keep_locked_file_settings( $merged, get_option( Vigilante_Settings::OPTION_NAME, array() ) ); | |
| 7071 | 7377 | update_option( Vigilante_Settings::OPTION_NAME, $merged ); |
| 7072 | 7378 | $this->settings->clear_cache(); |
| 7073 | 7379 | |
| 7074 | 7380 | // Re-evaluate the active preset marker. The imported config may match |
| @@ -7091,9 +7397,9 @@ | ||
| 7091 | 7397 | if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) { |
| 7092 | 7398 | wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' ); |
| 7093 | 7399 | } |
| 7094 | 7400 | |
| 7095 | - wp_send_json_success( __( 'Settings imported successfully.', 'vigilante' ) ); | |
| 7401 | + wp_send_json_success( __( 'Settings imported successfully.', 'vigilante' ) . $this->locked_file_settings_message() ); | |
| 7096 | 7402 | } |
| 7097 | 7403 | |
| 7098 | 7404 | /** |
| 7099 | 7405 | * Detect whether a vigilante_options array matches a known preset. |
| @@ -7196,9 +7502,11 @@ | ||
| 7196 | 7502 | $preset = isset( $_POST['preset'] ) ? sanitize_key( $_POST['preset'] ) : ''; |
| 7197 | 7503 | |
| 7198 | 7504 | // Handle reset to defaults |
| 7199 | 7505 | if ( 'reset' === $preset ) { |
| 7200 | - $defaults = Vigilante_Settings::get_defaults_preserving_user_data( get_option( Vigilante_Settings::OPTION_NAME, array() ) ); | |
| 7506 | + $stored_options = get_option( Vigilante_Settings::OPTION_NAME, array() ); | |
| 7507 | + $defaults = Vigilante_Settings::get_defaults_preserving_user_data( $stored_options ); | |
| 7508 | + $defaults = Vigilante_Settings::keep_locked_file_settings( $defaults, $stored_options ); | |
| 7201 | 7509 | update_option( Vigilante_Settings::OPTION_NAME, $defaults ); |
| 7202 | 7510 | $this->settings->clear_cache(); |
| 7203 | 7511 | |
| 7204 | 7512 | // Clear active preset |
| @@ -7206,9 +7514,9 @@ | ||
| 7206 | 7514 | |
| 7207 | 7515 | // Apply file changes after reset |
| 7208 | 7516 | $this->apply_all_file_changes( $defaults ); |
| 7209 | 7517 | |
| 7210 | - wp_send_json_success( __( 'Settings reset to defaults.', 'vigilante' ) ); | |
| 7518 | + wp_send_json_success( __( 'Settings reset to defaults.', 'vigilante' ) . $this->locked_file_settings_message() ); | |
| 7211 | 7519 | return; |
| 7212 | 7520 | } |
| 7213 | 7521 | |
| 7214 | 7522 | $presets = $this->settings->get_presets(); |
| @@ -7234,8 +7542,9 @@ | ||
| 7234 | 7542 | // invent keys that are missing on both sides. |
| 7235 | 7543 | $current = Vigilante_Settings::merge_preset( $this->settings->get_default_options(), $current ); |
| 7236 | 7544 | |
| 7237 | 7545 | $merged = Vigilante_Settings::merge_preset( $current, $preset_options ); |
| 7546 | + $merged = Vigilante_Settings::keep_locked_file_settings( $merged, get_option( Vigilante_Settings::OPTION_NAME, array() ) ); | |
| 7238 | 7547 | |
| 7239 | 7548 | update_option( Vigilante_Settings::OPTION_NAME, $merged ); |
| 7240 | 7549 | $this->settings->clear_cache(); |
| 7241 | 7550 | |
| @@ -7244,9 +7553,9 @@ | ||
| 7244 | 7553 | |
| 7245 | 7554 | // Apply file changes after preset |
| 7246 | 7555 | $this->apply_all_file_changes( $merged ); |
| 7247 | 7556 | |
| 7248 | - wp_send_json_success( __( 'Preset applied successfully.', 'vigilante' ) ); | |
| 7557 | + wp_send_json_success( __( 'Preset applied successfully.', 'vigilante' ) . $this->locked_file_settings_message() ); | |
| 7249 | 7558 | } |
| 7250 | 7559 | |
| 7251 | 7560 | /** |
| 7252 | 7561 | * AJAX: Reset a specific section to defaults |
| @@ -7281,27 +7590,19 @@ | ||
| 7281 | 7590 | * On a subsite, the settings written to wp-config.php and .htaccess are |
| 7282 | 7591 | * the main site's business. Resetting the local copy of those would only |
| 7283 | 7592 | * make this screen disagree with the file, so they are carried over |
| 7284 | 7593 | * untouched, and a section that is nothing but shared settings is not |
| 7285 | - * reset at all. | |
| 7594 | + * reset at all. On the main site, a user without network rights keeps | |
| 7595 | + * the ones the shared files are built from as well (2.11.6). | |
| 7286 | 7596 | */ |
| 7287 | - if ( ! Vigilante_Settings::can_write_shared_files() ) { | |
| 7288 | - $shared = Vigilante_Settings::get_shared_file_settings(); | |
| 7597 | + $locked = Vigilante_Settings::get_locked_file_settings(); | |
| 7289 | 7598 | |
| 7290 | - if ( isset( $shared[ $section ] ) ) { | |
| 7291 | - if ( true === $shared[ $section ] ) { | |
| 7292 | - wp_send_json_error( Vigilante_Settings::get_shared_files_notice() ); | |
| 7293 | - } | |
| 7294 | - | |
| 7295 | - foreach ( $shared[ $section ] as $shared_key ) { | |
| 7296 | - if ( array_key_exists( $shared_key, (array) $current_options[ $section ] ) ) { | |
| 7297 | - $new_values[ $shared_key ] = $current_options[ $section ][ $shared_key ]; | |
| 7298 | - } | |
| 7299 | - } | |
| 7300 | - } | |
| 7599 | + if ( isset( $locked[ $section ] ) && true === $locked[ $section ] ) { | |
| 7600 | + wp_send_json_error( Vigilante_Settings::get_shared_files_notice() ); | |
| 7301 | 7601 | } |
| 7302 | 7602 | |
| 7303 | 7603 | $current_options[ $section ] = $new_values; |
| 7604 | + $current_options = Vigilante_Settings::keep_locked_file_settings( $current_options, get_option( Vigilante_Settings::OPTION_NAME, array() ) ); | |
| 7304 | 7605 | |
| 7305 | 7606 | // Save |
| 7306 | 7607 | update_option( Vigilante_Settings::OPTION_NAME, $current_options ); |
| 7307 | 7608 | $this->settings->clear_cache(); |
| @@ -7384,8 +7685,19 @@ | ||
| 7384 | 7685 | // Save new results |
| 7385 | 7686 | update_option( 'vigilante_last_integrity_scan', time() ); |
| 7386 | 7687 | update_option( 'vigilante_last_integrity_results', $results ); |
| 7387 | 7688 | |
| 7689 | + // On the main site the scan does compute the lines of wp-config.php and | |
| 7690 | + // .htaccess, for the network administrator. Somebody without network | |
| 7691 | + // rights gets the change and its sizes, not the lines. | |
| 7692 | + if ( $this->critical_approval_locked() && ! empty( $results['modified'] ) && is_array( $results['modified'] ) ) { | |
| 7693 | + foreach ( $results['modified'] as $index => $item ) { | |
| 7694 | + if ( is_array( $item ) && 'critical_config' === ( $item['type'] ?? '' ) ) { | |
| 7695 | + $results['modified'][ $index ]['diff'] = Vigilante_File_Integrity::network_only_diff(); | |
| 7696 | + } | |
| 7697 | + } | |
| 7698 | + } | |
| 7699 | + | |
| 7388 | 7700 | wp_send_json_success( array( |
| 7389 | 7701 | 'message' => __( 'Scan completed.', 'vigilante' ), |
| 7390 | 7702 | 'results' => $results, |
| 7391 | 7703 | 'ignored_count' => count( get_option( 'vigilante_ignored_files', array() ) ), |
| @@ -7419,11 +7731,41 @@ | ||
| 7419 | 7731 | if ( ! current_user_can( 'manage_options' ) ) { |
| 7420 | 7732 | wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 7421 | 7733 | } |
| 7422 | 7734 | |
| 7735 | + $results = get_option( 'vigilante_last_integrity_results' ); | |
| 7736 | + $scanned_at = get_option( 'vigilante_last_integrity_scan' ); | |
| 7737 | + | |
| 7423 | 7738 | delete_option( 'vigilante_last_integrity_results' ); |
| 7424 | 7739 | delete_option( 'vigilante_last_integrity_scan' ); |
| 7425 | 7740 | |
| 7741 | + /* | |
| 7742 | + * A pending change to wp-config.php or the root .htaccess is closed by | |
| 7743 | + * approving it, which takes the network. Clearing the results was one | |
| 7744 | + * more way to close it without, until the next scan: the ignore list was | |
| 7745 | + * shut in 2.11.8 and this button was left open, found by the cross | |
| 7746 | + * review of 2.11.8. So for somebody who cannot approve, those entries | |
| 7747 | + * stay and everything else goes. | |
| 7748 | + */ | |
| 7749 | + if ( $this->critical_approval_locked() && is_array( $results ) && ! empty( $results['modified'] ) && is_array( $results['modified'] ) ) { | |
| 7750 | + $critical = array_values( | |
| 7751 | + array_filter( | |
| 7752 | + $results['modified'], | |
| 7753 | + function ( $item ) { | |
| 7754 | + return is_array( $item ) && 'critical_config' === ( $item['type'] ?? '' ); | |
| 7755 | + } | |
| 7756 | + ) | |
| 7757 | + ); | |
| 7758 | + | |
| 7759 | + if ( $critical ) { | |
| 7760 | + $results['modified'] = $critical; | |
| 7761 | + $results['suspicious'] = array(); | |
| 7762 | + $results['extra'] = array(); | |
| 7763 | + update_option( 'vigilante_last_integrity_results', $results ); | |
| 7764 | + update_option( 'vigilante_last_integrity_scan', $scanned_at ? $scanned_at : time() ); | |
| 7765 | + } | |
| 7766 | + } | |
| 7767 | + | |
| 7426 | 7768 | if ( $this->database ) { |
| 7427 | 7769 | $this->database->clear_file_hashes(); |
| 7428 | 7770 | } |
| 7429 | 7771 | |
| @@ -7449,8 +7791,14 @@ | ||
| 7449 | 7791 | if ( empty( $file ) ) { |
| 7450 | 7792 | wp_send_json_error( __( 'No file specified.', 'vigilante' ) ); |
| 7451 | 7793 | } |
| 7452 | 7794 | |
| 7795 | + // A change to a shared file is closed by approving it, and approving it | |
| 7796 | + // takes the network. Ignoring it would close the same warning without. | |
| 7797 | + if ( $this->critical_approval_locked() && in_array( $file, array( 'wp-config.php', '.htaccess' ), true ) ) { | |
| 7798 | + wp_send_json_error( $this->critical_approval_notice() ); | |
| 7799 | + } | |
| 7800 | + | |
| 7453 | 7801 | $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database ); |
| 7454 | 7802 | $file_integrity->ignore_file( $file ); |
| 7455 | 7803 | |
| 7456 | 7804 | // Also remove the file from stored scan results so UI updates |
| @@ -7514,12 +7862,14 @@ | ||
| 7514 | 7862 | if ( ! is_array( $raw_files ) ) { |
| 7515 | 7863 | wp_send_json_error( __( 'Invalid request.', 'vigilante' ) ); |
| 7516 | 7864 | } |
| 7517 | 7865 | |
| 7518 | - $files = array(); | |
| 7866 | + $files = array(); | |
| 7867 | + $shared = $this->critical_approval_locked() ? array( 'wp-config.php', '.htaccess' ) : array(); | |
| 7519 | 7868 | foreach ( $raw_files as $f ) { |
| 7520 | 7869 | $clean = sanitize_text_field( $f ); |
| 7521 | - if ( '' !== $clean ) { | |
| 7870 | + // Same rule as ajax_ignore_file() for the two shared files. | |
| 7871 | + if ( '' !== $clean && ! in_array( $clean, $shared, true ) ) { | |
| 7522 | 7872 | $files[] = $clean; |
| 7523 | 7873 | } |
| 7524 | 7874 | } |
| 7525 | 7875 | |