PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.8
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.8
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
← All changes | admin/class-admin.php +399 -47 2.10.02.11.8 View file →
@@ -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' ),
@@ -1559,8 +1605,9 @@
1559 1605 'logType' => __( 'Type', 'vigilante' ),
1560 1606 'logAction' => __( 'Action', 'vigilante' ),
1561 1607 'logSeverity' => __( 'Severity', 'vigilante' ),
1562 1608 'logMessage' => __( 'Message', 'vigilante' ),
1609 + 'logRequestUri' => __( 'Address', 'vigilante' ),
1563 1610 'logClient' => __( 'Client', 'vigilante' ),
1564 1611 'logUser' => __( 'User', 'vigilante' ),
1565 1612 'logIpAddress' => __( 'IP Address', 'vigilante' ),
1566 1613 'logUserAgent' => __( 'User Agent', 'vigilante' ),
@@ -1765,8 +1812,21 @@
1765 1812 </p>
1766 1813 <p>
1767 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>
1768 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; ?>
1769 1829 </div>
1770 1830 <?php
1771 1831 }
1772 1832 }
@@ -1998,8 +2058,41 @@
1998 2058 return ! Vigilante_Settings::can_write_shared_files();
1999 2059 }
2000 2060
2001 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 + /**
2002 2095 * Print the shared-files notice for a section that cannot be edited here
2003 2096 *
2004 2097 * @since 2.9.8
2005 2098 */
@@ -2014,8 +2107,109 @@
2014 2107 <?php
2015 2108 }
2016 2109
2017 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 + /**
2018 2212 * Check if module is disabled and render warning
2019 2213 *
2020 2214 * @param string $module_key Module key.
2021 2215 * @return bool True if disabled.
@@ -2559,14 +2753,15 @@
2559 2753
2560 2754 <?php $this->render_analyzer_widget( $analyzer_last_scan, $analyzer_history, $analyzer_categories_def, $analyzer_settings ); ?>
2561 2755
2562 2756 <div class="vigilante-modules-grid">
2563 - <h2><?php esc_html_e( 'Security Modules', 'vigilante' ); ?></h2>
2757 + <h2 id="vigilante-section-dashboard-modules"><?php esc_html_e( 'Security Modules', 'vigilante' ); ?></h2>
2564 2758 <p class="description"><?php esc_html_e( 'Enable or disable security modules. Each module controls a tab with detailed settings.', 'vigilante' ); ?></p>
2565 2759 <div class="vigilante-modules-list">
2566 2760 <?php foreach ( $options['modules'] as $module => $enabled ) :
2567 2761 $label = isset( $module_labels[ $module ] ) ? $module_labels[ $module ] : ucwords( str_replace( '_', ' ', $module ) );
2568 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 );
2569 2764 ?>
2570 2765 <div class="vigilante-module-item <?php echo $enabled ? 'enabled' : 'disabled'; ?>">
2571 2766 <div class="vigilante-module-header">
2572 2767 <span class="vigilante-module-status"></span>
@@ -2579,8 +2774,9 @@
2579 2774 <input type="checkbox"
2580 2775 name="modules[<?php echo esc_attr( $module ); ?>]"
2581 2776 value="1"
2582 2777 <?php checked( $enabled ); ?>
2778 + <?php disabled( $vg_module_locked ); ?>
2583 2779 aria-label="<?php echo esc_attr( $toggle_label ); ?>"
2584 2780 data-module="<?php echo esc_attr( $module ); ?>">
2585 2781 <span class="vigilante-toggle-slider"></span>
2586 2782 </label>
@@ -2587,8 +2783,11 @@
2587 2783 </div>
2588 2784 <?php if ( $description ) : ?>
2589 2785 <p class="vigilante-module-desc"><?php echo esc_html( $description ); ?></p>
2590 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; ?>
2591 2790 </div>
2592 2791 <?php endforeach; ?>
2593 2792 </div>
2594 2793 </div>
@@ -2620,9 +2819,9 @@
2620 2819 $ua_remaining_hours = floor( $ua_remaining / 3600 );
2621 2820 $ua_remaining_mins = floor( ( $ua_remaining % 3600 ) / 60 );
2622 2821 ?>
2623 2822 <div class="vigilante-preset-card vigilante-under-attack-card <?php echo $ua_active ? 'vigilante-under-attack-active' : ''; ?>">
2624 - <h3>
2823 + <h3 id="vigilante-section-dashboard-under-attack">
2625 2824 <span class="dashicons dashicons-shield"></span>
2626 2825 <?php esc_html_e( 'Under Attack', 'vigilante' ); ?>
2627 2826 </h3>
2628 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>
@@ -2978,14 +3177,21 @@
2978 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' ); ?>
2979 3178 </p>
2980 3179 </div>
2981 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 +
2982 3188 <table class="form-table">
2983 3189 <tr>
2984 3190 <th scope="row"><?php esc_html_e( 'Block Bad Query Strings', 'vigilante' ); ?></th>
2985 3191 <td>
2986 3192 <label>
2987 - <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'] ) ); ?>>
2988 3194 <?php esc_html_e( 'Block malicious query string patterns', 'vigilante' ); ?>
2989 3195 </label>
2990 3196 </td>
2991 3197 </tr>
@@ -3028,9 +3234,9 @@
3028 3234 <tr>
3029 3235 <th scope="row"><?php esc_html_e( 'Block Bad Bots', 'vigilante' ); ?></th>
3030 3236 <td>
3031 3237 <label>
3032 - <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'] ) ); ?>>
3033 3239 <?php esc_html_e( 'Block known malicious bots and scanners', 'vigilante' ); ?>
3034 3240 </label>
3035 3241 </td>
3036 3242 </tr>
@@ -3144,8 +3350,29 @@
3144 3350 </table>
3145 3351 </div>
3146 3352 <?php endif; ?>
3147 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 +
3148 3375 <h3><?php esc_html_e( 'IP Lists', 'vigilante' ); ?></h3>
3149 3376 <p class="description">
3150 3377 <?php
3151 3378 printf(
@@ -3159,9 +3386,9 @@
3159 3386 <tr>
3160 3387 <th scope="row"><label for="vigilante-f-firewall-trusted-proxy-header"><?php esc_html_e( 'Visitor IP detection', 'vigilante' ); ?></label></th>
3161 3388 <td>
3162 3389 <?php $proxy_header = $options['trusted_proxy_header'] ?? ''; ?>
3163 - <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 ); ?>>
3164 3391 <option value="" <?php selected( $proxy_header, '' ); ?>><?php esc_html_e( 'Direct connection, only REMOTE_ADDR (recommended)', 'vigilante' ); ?></option>
3165 3392 <option value="cf-connecting-ip" <?php selected( $proxy_header, 'cf-connecting-ip' ); ?>><?php esc_html_e( 'Behind Cloudflare (CF-Connecting-IP)', 'vigilante' ); ?></option>
3166 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>
3167 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>
@@ -3173,9 +3400,9 @@
3173 3400 </tr>
3174 3401 <tr>
3175 3402 <th scope="row"><label for="vigilante-f-firewall-ip-whitelist"><?php esc_html_e( 'IP Whitelist', 'vigilante' ); ?></label></th>
3176 3403 <td>
3177 - <textarea id="vigilante-f-firewall-ip-whitelist" name="firewall[ip_whitelist]" rows="4" class="large-text code" placeholder="192.168.1.50&#10;192.168.1.0/24&#10;192.168.1.*"><?php echo esc_textarea( implode( "\n", $options['ip_whitelist'] ?? array() ) ); ?></textarea>
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&#10;192.168.1.0/24&#10;192.168.1.*"><?php echo esc_textarea( implode( "\n", $options['ip_whitelist'] ?? array() ) ); ?></textarea>
3178 3405 <p class="description">
3179 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' ); ?>
3180 3407 <br>
3181 3408 <?php
@@ -3214,9 +3441,9 @@
3214 3441 <table class="form-table">
3215 3442 <tr>
3216 3443 <th scope="row"><label for="vigilante-f-firewall-ua-whitelist"><?php esc_html_e( 'User-Agent Whitelist', 'vigilante' ); ?></label></th>
3217 3444 <td>
3218 - <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>
3219 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>
3220 3447 </td>
3221 3448 </tr>
3222 3449 <tr>
@@ -3504,9 +3731,9 @@
3504 3731 $two_factor = $options['two_factor'] ?? array();
3505 3732 $two_factor_enabled = ! empty( $two_factor['enabled'] );
3506 3733 ?>
3507 3734 <div class="vigilante-settings-section vigilante-lockout-section">
3508 - <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>
3509 3736
3510 3737 <table class="form-table">
3511 3738 <tr>
3512 3739 <th scope="row"><?php esc_html_e( 'Current settings', 'vigilante' ); ?></th>
@@ -3666,9 +3893,9 @@
3666 3893 $excluded = $two_factor['excluded_users'] ?? array();
3667 3894 $method = $two_factor['method'] ?? 'email';
3668 3895 $grace_days = $two_factor['grace_period_days'] ?? 3;
3669 3896 ?>
3670 - <h3>
3897 + <h3 id="vigilante-section-login-2fa">
3671 3898 <?php esc_html_e( 'Two-Factor Authentication (2FA)', 'vigilante' ); ?>
3672 3899 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
3673 3900 <span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span>
3674 3901 </h3>
@@ -4039,9 +4266,9 @@
4039 4266 </td>
4040 4267 </tr>
4041 4268 </table>
4042 4269
4043 - <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>
4044 4271 <table class="form-table">
4045 4272 <tr>
4046 4273 <th scope="row"><?php esc_html_e( 'Enable CSP', 'vigilante' ); ?></th>
4047 4274 <td>
@@ -4061,9 +4288,9 @@
4061 4288 </td>
4062 4289 </tr>
4063 4290 </table>
4064 4291
4065 - <h3><?php esc_html_e( 'HTTPS', 'vigilante' ); ?></h3>
4292 + <h3 id="vigilante-section-headers-force-https"><?php esc_html_e( 'HTTPS', 'vigilante' ); ?></h3>
4066 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>
4067 4294 <table class="form-table">
4068 4295 <tr>
4069 4296 <th scope="row"><?php esc_html_e( 'Redirect HTTP to HTTPS', 'vigilante' ); ?></th>
@@ -4106,9 +4333,9 @@
4106 4333 </td>
4107 4334 </tr>
4108 4335 </table>
4109 4336
4110 - <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>
4111 4338 <?php $vig_home_https = ( 0 === strpos( (string) get_option( 'home' ), 'https://' ) ); ?>
4112 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>
4113 4340 <?php if ( ! $vig_home_https ) : ?>
4114 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>
@@ -4149,9 +4376,9 @@
4149 4376 </td>
4150 4377 </tr>
4151 4378 </table>
4152 4379
4153 - <h3><?php esc_html_e( 'Server Identity', 'vigilante' ); ?></h3>
4380 + <h3 id="vigilante-section-headers-fingerprint"><?php esc_html_e( 'Server Identity', 'vigilante' ); ?></h3>
4154 4381 <p class="description"><?php esc_html_e( 'Hide identifying information that servers expose in responses.', 'vigilante' ); ?></p>
4155 4382 <table class="form-table">
4156 4383 <tr>
4157 4384 <th scope="row"><?php esc_html_e( 'Server Signature', 'vigilante' ); ?></th>
@@ -4740,8 +4967,11 @@
4740 4967 <h2 class="vigilante-tools-header">
4741 4968 <?php esc_html_e( 'User security tools', 'vigilante' ); ?>
4742 4969 </h2>
4743 4970
4971 + <?php $this->render_user_actions_notice(); ?>
4972 + <?php if ( ! $this->user_actions_locked() ) : ?>
4973 +
4744 4974 <!-- Force Password Reset -->
4745 4975 <div class="vigilante-tool-box">
4746 4976 <h3><?php esc_html_e( 'Force password reset', 'vigilante' ); ?></h3>
4747 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>
@@ -4877,9 +5107,9 @@
4877 5107 <?php
4878 5108 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
4879 5109 $pending_users = $user_security->get_pending_users();
4880 5110 ?>
4881 - <div class="vigilante-tool-box vigilante-pending-users-section">
5111 + <div id="vigilante-section-users-pending" class="vigilante-tool-box vigilante-pending-users-section">
4882 5112 <h3>
4883 5113 <?php esc_html_e( 'Pending registrations', 'vigilante' ); ?>
4884 5114 <?php if ( count( $pending_users ) > 0 ) : ?>
4885 5115 <span class="vigilante-badge vigilante-badge-warning"><?php echo esc_html( count( $pending_users ) ); ?></span>
@@ -4896,8 +5126,9 @@
4896 5126 <span class="dashicons dashicons-yes-alt"></span>
4897 5127 <p><?php esc_html_e( 'No pending registrations.', 'vigilante' ); ?></p>
4898 5128 </div>
4899 5129 <?php else : ?>
5130 + <?php $this->render_user_actions_notice(); ?>
4900 5131 <table class="wp-list-table widefat fixed striped vigilante-pending-users-table">
4901 5132 <thead>
4902 5133 <tr>
4903 5134 <th><?php esc_html_e( 'User', 'vigilante' ); ?></th>
@@ -4926,12 +5157,12 @@
4926 5157 }
4927 5158 ?>
4928 5159 </td>
4929 5160 <td>
4930 - <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() ); ?>>
4931 5162 <?php esc_html_e( 'Approve', 'vigilante' ); ?>
4932 5163 </button>
4933 - <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() ); ?>>
4934 5165 <?php esc_html_e( 'Reject', 'vigilante' ); ?>
4935 5166 </button>
4936 5167 </td>
4937 5168 </tr>
@@ -5053,8 +5284,10 @@
5053 5284 </button>
5054 5285 </p>
5055 5286 </div>
5056 5287 </div>
5288 +
5289 + <?php endif; ?>
5057 5290 </div>
5058 5291 <?php
5059 5292 }
5060 5293
@@ -5768,8 +6001,9 @@
5768 6001 'user' => (string) ( $log->user_login ?? '' ),
5769 6002 'ip' => $ip_val,
5770 6003 'user_agent' => $ua_val,
5771 6004 'request_method' => (string) $request_method,
6005 + 'request_uri' => Vigilante_Activity_Log::extract_request_uri( $log->extra_data ?? '' ),
5772 6006 'date' => (string) ( $log->created_at ?? '' ),
5773 6007 'severity' => (string) ( $log->severity ?? 'info' ),
5774 6008 'is_ip_whitelisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_whitelist, true ) ),
5775 6009 'is_ip_blacklisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_blacklist, true ) ),
@@ -5822,8 +6056,13 @@
5822 6056 */
5823 6057 private function render_tab_file_integrity() {
5824 6058 $is_disabled = $this->render_module_disabled_notice( 'file_integrity' );
5825 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();
5826 6065 $last_scan = get_option( 'vigilante_last_integrity_scan' );
5827 6066 $last_results = get_option( 'vigilante_last_integrity_results' );
5828 6067 $ignored_files = get_option( 'vigilante_ignored_files', array() );
5829 6068
@@ -5949,10 +6188,13 @@
5949 6188 <?php esc_html_e( 'Uploads directory (detect PHP files, double extensions, .htaccess)', 'vigilante' ); ?>
5950 6189 </label>
5951 6190 <br>
5952 6191 <label>
5953 - <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 ); ?>>
5954 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; ?>
5955 6197 </label>
5956 6198 <br>
5957 6199 <label>
5958 6200 <input type="checkbox" name="file_integrity[check_closed_plugins]" value="1" <?php checked( $options['check_closed_plugins'] ?? true ); ?>>
@@ -6231,9 +6473,15 @@
6231 6473 $crit_diff = $crit_item['diff'] ?? array();
6232 6474 $crit_id = sanitize_html_class( $crit_file );
6233 6475 $added_count = is_array( $crit_diff ) ? count( $crit_diff['added'] ?? array() ) : 0;
6234 6476 $removed_count = is_array( $crit_diff ) ? count( $crit_diff['removed'] ?? array() ) : 0;
6235 - $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'] ) );
6236 6484 ?>
6237 6485 <tr>
6238 6486 <td><code style="color: #e36210;"><?php echo esc_html( $crit_file ); ?></code></td>
6239 6487 <td>
@@ -6256,18 +6504,36 @@
6256 6504 <td>
6257 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' ); ?>">
6258 6506 <?php esc_html_e( 'Review changes', 'vigilante' ); ?>
6259 6507 </button>
6260 - <button type="button" class="button button-small button-primary vigilante-approve-critical-file" data-file="<?php echo esc_attr( $crit_file ); ?>">
6261 - <?php esc_html_e( 'Approve', 'vigilante' ); ?>
6262 - </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; ?>
6263 6517 </td>
6264 6518 </tr>
6265 6519 <tr id="vigilante-critical-content-<?php echo esc_attr( $crit_id ); ?>" class="vigilante-critical-content-row" style="display:none;">
6266 6520 <td colspan="3" style="padding: 0;">
6267 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;">
6268 - <?php if ( $diff_unavailable ) : ?>
6522 + <?php if ( $diff_network ) : ?>
6269 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;">
6270 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' ); ?>
6271 6537 </p>
6272 6538 <?php elseif ( empty( $crit_diff['added'] ) && empty( $crit_diff['removed'] ) ) : ?>
6273 6539 <p style="color: #50575e; font-style: italic; margin: 0;">
@@ -6295,9 +6561,9 @@
6295 6561 <?php endif; ?>
6296 6562
6297 6563 <?php if ( $has_closed ) : ?>
6298 6564 <div class="vigilante-file-list vigilante-closed-plugins">
6299 - <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>
6300 6566 <p class="description" style="color: #d63638;">
6301 6567 <?php esc_html_e( '&#9888; Warning: These plugins have been closed in the WordPress.org repository. Closures usually indicate malware, security issues, guideline violations, or supply chain attacks. Uninstall and replace as soon as possible.', 'vigilante' ); ?>
6302 6568 </p>
6303 6569 <table class="wp-list-table widefat striped">
@@ -6506,8 +6772,15 @@
6506 6772 if ( ! current_user_can( 'manage_options' ) ) {
6507 6773 wp_die( esc_html__( 'Permission denied.', 'vigilante' ), 403 );
6508 6774 }
6509 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 +
6510 6783 $backup_manager = new Vigilante_Backup_Manager();
6511 6784 $result = $backup_manager->stream_files_zip();
6512 6785
6513 6786 // stream_files_zip() exits on success; only a WP_Error returns here.
@@ -6596,8 +6869,27 @@
6596 6869
6597 6870 // Read ONLY saved options from database (not merged with defaults)
6598 6871 $saved_options = get_option( Vigilante_Settings::OPTION_NAME, array() );
6599 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 +
6600 6892 $rejected_ips = array();
6601 6893
6602 6894 // Handle modules
6603 6895 if ( 'modules' === $section && isset( $data['modules'] ) ) {
@@ -6636,8 +6928,10 @@
6636 6928
6637 6929 // Clear cache before saving
6638 6930 wp_cache_delete( Vigilante_Settings::OPTION_NAME, 'options' );
6639 6931
6932 + $saved_options = Vigilante_Settings::keep_locked_file_settings( $saved_options, $stored_options );
6933 +
6640 6934 // Save to database
6641 6935 update_option( Vigilante_Settings::OPTION_NAME, $saved_options );
6642 6936
6643 6937 // Clear the settings cache
@@ -7060,13 +7354,27 @@
7060 7354
7061 7355 // Sanitize imported data recursively
7062 7356 $imported = map_deep( $imported, 'sanitize_text_field' );
7063 7357
7064 - // Validate structure
7065 - $defaults = $this->settings->get_default_options();
7066 - $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;
7067 7368
7369 + foreach ( $validated as $section => $data ) {
7370 + if ( is_array( $data ) ) {
7371 + $merged[ $section ] = $data;
7372 + }
7373 + }
7374 +
7068 7375 // Save
7376 + $merged = Vigilante_Settings::keep_locked_file_settings( $merged, get_option( Vigilante_Settings::OPTION_NAME, array() ) );
7069 7377 update_option( Vigilante_Settings::OPTION_NAME, $merged );
7070 7378 $this->settings->clear_cache();
7071 7379
7072 7380 // Re-evaluate the active preset marker. The imported config may match
@@ -7089,9 +7397,9 @@
7089 7397 if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) {
7090 7398 wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' );
7091 7399 }
7092 7400
7093 - wp_send_json_success( __( 'Settings imported successfully.', 'vigilante' ) );
7401 + wp_send_json_success( __( 'Settings imported successfully.', 'vigilante' ) . $this->locked_file_settings_message() );
7094 7402 }
7095 7403
7096 7404 /**
7097 7405 * Detect whether a vigilante_options array matches a known preset.
@@ -7194,9 +7502,11 @@
7194 7502 $preset = isset( $_POST['preset'] ) ? sanitize_key( $_POST['preset'] ) : '';
7195 7503
7196 7504 // Handle reset to defaults
7197 7505 if ( 'reset' === $preset ) {
7198 - $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 );
7199 7509 update_option( Vigilante_Settings::OPTION_NAME, $defaults );
7200 7510 $this->settings->clear_cache();
7201 7511
7202 7512 // Clear active preset
@@ -7204,9 +7514,9 @@
7204 7514
7205 7515 // Apply file changes after reset
7206 7516 $this->apply_all_file_changes( $defaults );
7207 7517
7208 - wp_send_json_success( __( 'Settings reset to defaults.', 'vigilante' ) );
7518 + wp_send_json_success( __( 'Settings reset to defaults.', 'vigilante' ) . $this->locked_file_settings_message() );
7209 7519 return;
7210 7520 }
7211 7521
7212 7522 $presets = $this->settings->get_presets();
@@ -7232,8 +7542,9 @@
7232 7542 // invent keys that are missing on both sides.
7233 7543 $current = Vigilante_Settings::merge_preset( $this->settings->get_default_options(), $current );
7234 7544
7235 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() ) );
7236 7547
7237 7548 update_option( Vigilante_Settings::OPTION_NAME, $merged );
7238 7549 $this->settings->clear_cache();
7239 7550
@@ -7242,9 +7553,9 @@
7242 7553
7243 7554 // Apply file changes after preset
7244 7555 $this->apply_all_file_changes( $merged );
7245 7556
7246 - wp_send_json_success( __( 'Preset applied successfully.', 'vigilante' ) );
7557 + wp_send_json_success( __( 'Preset applied successfully.', 'vigilante' ) . $this->locked_file_settings_message() );
7247 7558 }
7248 7559
7249 7560 /**
7250 7561 * AJAX: Reset a specific section to defaults
@@ -7279,27 +7590,19 @@
7279 7590 * On a subsite, the settings written to wp-config.php and .htaccess are
7280 7591 * the main site's business. Resetting the local copy of those would only
7281 7592 * make this screen disagree with the file, so they are carried over
7282 7593 * untouched, and a section that is nothing but shared settings is not
7283 - * 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).
7284 7596 */
7285 - if ( ! Vigilante_Settings::can_write_shared_files() ) {
7286 - $shared = Vigilante_Settings::get_shared_file_settings();
7597 + $locked = Vigilante_Settings::get_locked_file_settings();
7287 7598
7288 - if ( isset( $shared[ $section ] ) ) {
7289 - if ( true === $shared[ $section ] ) {
7290 - wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
7291 - }
7292 -
7293 - foreach ( $shared[ $section ] as $shared_key ) {
7294 - if ( array_key_exists( $shared_key, (array) $current_options[ $section ] ) ) {
7295 - $new_values[ $shared_key ] = $current_options[ $section ][ $shared_key ];
7296 - }
7297 - }
7298 - }
7599 + if ( isset( $locked[ $section ] ) && true === $locked[ $section ] ) {
7600 + wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
7299 7601 }
7300 7602
7301 7603 $current_options[ $section ] = $new_values;
7604 + $current_options = Vigilante_Settings::keep_locked_file_settings( $current_options, get_option( Vigilante_Settings::OPTION_NAME, array() ) );
7302 7605
7303 7606 // Save
7304 7607 update_option( Vigilante_Settings::OPTION_NAME, $current_options );
7305 7608 $this->settings->clear_cache();
@@ -7382,8 +7685,19 @@
7382 7685 // Save new results
7383 7686 update_option( 'vigilante_last_integrity_scan', time() );
7384 7687 update_option( 'vigilante_last_integrity_results', $results );
7385 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 +
7386 7700 wp_send_json_success( array(
7387 7701 'message' => __( 'Scan completed.', 'vigilante' ),
7388 7702 'results' => $results,
7389 7703 'ignored_count' => count( get_option( 'vigilante_ignored_files', array() ) ),
@@ -7417,11 +7731,41 @@
7417 7731 if ( ! current_user_can( 'manage_options' ) ) {
7418 7732 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
7419 7733 }
7420 7734
7735 + $results = get_option( 'vigilante_last_integrity_results' );
7736 + $scanned_at = get_option( 'vigilante_last_integrity_scan' );
7737 +
7421 7738 delete_option( 'vigilante_last_integrity_results' );
7422 7739 delete_option( 'vigilante_last_integrity_scan' );
7423 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 +
7424 7768 if ( $this->database ) {
7425 7769 $this->database->clear_file_hashes();
7426 7770 }
7427 7771
@@ -7447,8 +7791,14 @@
7447 7791 if ( empty( $file ) ) {
7448 7792 wp_send_json_error( __( 'No file specified.', 'vigilante' ) );
7449 7793 }
7450 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 +
7451 7801 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
7452 7802 $file_integrity->ignore_file( $file );
7453 7803
7454 7804 // Also remove the file from stored scan results so UI updates
@@ -7512,12 +7862,14 @@
7512 7862 if ( ! is_array( $raw_files ) ) {
7513 7863 wp_send_json_error( __( 'Invalid request.', 'vigilante' ) );
7514 7864 }
7515 7865
7516 - $files = array();
7866 + $files = array();
7867 + $shared = $this->critical_approval_locked() ? array( 'wp-config.php', '.htaccess' ) : array();
7517 7868 foreach ( $raw_files as $f ) {
7518 7869 $clean = sanitize_text_field( $f );
7519 - if ( '' !== $clean ) {
7870 + // Same rule as ajax_ignore_file() for the two shared files.
7871 + if ( '' !== $clean && ! in_array( $clean, $shared, true ) ) {
7520 7872 $files[] = $clean;
7521 7873 }
7522 7874 }
7523 7875