| @@ -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 |
| @@ -1448,8 +1468,11 @@ | ||
| 1448 | 1468 | 'currentUserId' => get_current_user_id(), |
| 1449 | 1469 | 'logoutUrl' => wp_logout_url( wp_login_url() ), |
| 1450 | 1470 | 'adminUrl' => admin_url( 'admin.php?page=vigilante' ), |
| 1451 | 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(), | |
| 1452 | 1475 | 'underAttack' => array( |
| 1453 | 1476 | 'active' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->is_active(), |
| 1454 | 1477 | 'remaining' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->get_remaining_time(), |
| 1455 | 1478 | ), |
| @@ -1516,13 +1539,17 @@ | ||
| 1516 | 1539 | 'criticalConfigTitle' => __( 'Critical config files modified', 'vigilante' ), |
| 1517 | 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' ), |
| 1518 | 1541 | 'approve' => __( 'Approve', 'vigilante' ), |
| 1519 | 1542 | 'approving' => __( 'Approving...', 'vigilante' ), |
| 1543 | + 'approvalLockedNotice' => $this->critical_approval_notice(), | |
| 1520 | 1544 | 'criticalApproved' => __( 'Change approved. Next scan will use the current state as baseline.', 'vigilante' ), |
| 1521 | 1545 | 'reviewChanges' => __( 'Review changes', 'vigilante' ), |
| 1522 | 1546 | 'hideChanges' => __( 'Hide changes', 'vigilante' ), |
| 1523 | 1547 | 'changes' => __( 'Changes', 'vigilante' ), |
| 1524 | 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' ), | |
| 1525 | 1552 | 'diffEmpty' => __( 'No line-level changes detected (may be whitespace or reordering).', 'vigilante' ), |
| 1526 | 1553 | 'diffLines' => __( 'lines', 'vigilante' ), |
| 1527 | 1554 | // Under Attack mode strings |
| 1528 | 1555 | 'underAttackConfirmActivate' => __( 'Activate Under Attack mode? All visitors will see a verification page for the next 4 hours.', 'vigilante' ), |
| @@ -2031,8 +2058,41 @@ | ||
| 2031 | 2058 | return ! Vigilante_Settings::can_write_shared_files(); |
| 2032 | 2059 | } |
| 2033 | 2060 | |
| 2034 | 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 | + /** | |
| 2035 | 2095 | * Print the shared-files notice for a section that cannot be edited here |
| 2036 | 2096 | * |
| 2037 | 2097 | * @since 2.9.8 |
| 2038 | 2098 | */ |
| @@ -2058,10 +2118,46 @@ | ||
| 2058 | 2118 | * |
| 2059 | 2119 | * @since 2.10.4 |
| 2060 | 2120 | * @return bool |
| 2061 | 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 | + */ | |
| 2062 | 2155 | private function user_actions_locked() { |
| 2063 | - return is_multisite() && ! current_user_can( 'manage_network_users' ); | |
| 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' ); | |
| 2064 | 2160 | } |
| 2065 | 2161 | |
| 2066 | 2162 | /** |
| 2067 | 2163 | * Print the notice for user tools that cannot be used from this site |
| @@ -2073,14 +2169,47 @@ | ||
| 2073 | 2169 | return; |
| 2074 | 2170 | } |
| 2075 | 2171 | ?> |
| 2076 | 2172 | <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;"> |
| 2173 | + <?php if ( is_multisite() ) : ?> | |
| 2077 | 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; ?> | |
| 2078 | 2178 | </div> |
| 2079 | 2179 | <?php |
| 2080 | 2180 | } |
| 2081 | 2181 | |
| 2082 | 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 | + /** | |
| 2083 | 2212 | * Check if module is disabled and render warning |
| 2084 | 2213 | * |
| 2085 | 2214 | * @param string $module_key Module key. |
| 2086 | 2215 | * @return bool True if disabled. |
| @@ -2630,8 +2759,9 @@ | ||
| 2630 | 2759 | <div class="vigilante-modules-list"> |
| 2631 | 2760 | <?php foreach ( $options['modules'] as $module => $enabled ) : |
| 2632 | 2761 | $label = isset( $module_labels[ $module ] ) ? $module_labels[ $module ] : ucwords( str_replace( '_', ' ', $module ) ); |
| 2633 | 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 ); | |
| 2634 | 2764 | ?> |
| 2635 | 2765 | <div class="vigilante-module-item <?php echo $enabled ? 'enabled' : 'disabled'; ?>"> |
| 2636 | 2766 | <div class="vigilante-module-header"> |
| 2637 | 2767 | <span class="vigilante-module-status"></span> |
| @@ -2644,8 +2774,9 @@ | ||
| 2644 | 2774 | <input type="checkbox" |
| 2645 | 2775 | name="modules[<?php echo esc_attr( $module ); ?>]" |
| 2646 | 2776 | value="1" |
| 2647 | 2777 | <?php checked( $enabled ); ?> |
| 2778 | + <?php disabled( $vg_module_locked ); ?> | |
| 2648 | 2779 | aria-label="<?php echo esc_attr( $toggle_label ); ?>" |
| 2649 | 2780 | data-module="<?php echo esc_attr( $module ); ?>"> |
| 2650 | 2781 | <span class="vigilante-toggle-slider"></span> |
| 2651 | 2782 | </label> |
| @@ -2652,8 +2783,11 @@ | ||
| 2652 | 2783 | </div> |
| 2653 | 2784 | <?php if ( $description ) : ?> |
| 2654 | 2785 | <p class="vigilante-module-desc"><?php echo esc_html( $description ); ?></p> |
| 2655 | 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; ?> | |
| 2656 | 2790 | </div> |
| 2657 | 2791 | <?php endforeach; ?> |
| 2658 | 2792 | </div> |
| 2659 | 2793 | </div> |
| @@ -3043,14 +3177,21 @@ | ||
| 3043 | 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' ); ?> |
| 3044 | 3178 | </p> |
| 3045 | 3179 | </div> |
| 3046 | 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 | + | |
| 3047 | 3188 | <table class="form-table"> |
| 3048 | 3189 | <tr> |
| 3049 | 3190 | <th scope="row"><?php esc_html_e( 'Block Bad Query Strings', 'vigilante' ); ?></th> |
| 3050 | 3191 | <td> |
| 3051 | 3192 | <label> |
| 3052 | - <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'] ) ); ?>> | |
| 3053 | 3194 | <?php esc_html_e( 'Block malicious query string patterns', 'vigilante' ); ?> |
| 3054 | 3195 | </label> |
| 3055 | 3196 | </td> |
| 3056 | 3197 | </tr> |
| @@ -3093,9 +3234,9 @@ | ||
| 3093 | 3234 | <tr> |
| 3094 | 3235 | <th scope="row"><?php esc_html_e( 'Block Bad Bots', 'vigilante' ); ?></th> |
| 3095 | 3236 | <td> |
| 3096 | 3237 | <label> |
| 3097 | - <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'] ) ); ?>> | |
| 3098 | 3239 | <?php esc_html_e( 'Block known malicious bots and scanners', 'vigilante' ); ?> |
| 3099 | 3240 | </label> |
| 3100 | 3241 | </td> |
| 3101 | 3242 | </tr> |
| @@ -3209,8 +3350,29 @@ | ||
| 3209 | 3350 | </table> |
| 3210 | 3351 | </div> |
| 3211 | 3352 | <?php endif; ?> |
| 3212 | 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 | + | |
| 3213 | 3375 | <h3><?php esc_html_e( 'IP Lists', 'vigilante' ); ?></h3> |
| 3214 | 3376 | <p class="description"> |
| 3215 | 3377 | <?php |
| 3216 | 3378 | printf( |
| @@ -3224,9 +3386,9 @@ | ||
| 3224 | 3386 | <tr> |
| 3225 | 3387 | <th scope="row"><label for="vigilante-f-firewall-trusted-proxy-header"><?php esc_html_e( 'Visitor IP detection', 'vigilante' ); ?></label></th> |
| 3226 | 3388 | <td> |
| 3227 | 3389 | <?php $proxy_header = $options['trusted_proxy_header'] ?? ''; ?> |
| 3228 | - <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 ); ?>> | |
| 3229 | 3391 | <option value="" <?php selected( $proxy_header, '' ); ?>><?php esc_html_e( 'Direct connection, only REMOTE_ADDR (recommended)', 'vigilante' ); ?></option> |
| 3230 | 3392 | <option value="cf-connecting-ip" <?php selected( $proxy_header, 'cf-connecting-ip' ); ?>><?php esc_html_e( 'Behind Cloudflare (CF-Connecting-IP)', 'vigilante' ); ?></option> |
| 3231 | 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> |
| 3232 | 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> |
| @@ -3238,9 +3400,9 @@ | ||
| 3238 | 3400 | </tr> |
| 3239 | 3401 | <tr> |
| 3240 | 3402 | <th scope="row"><label for="vigilante-f-firewall-ip-whitelist"><?php esc_html_e( 'IP Whitelist', 'vigilante' ); ?></label></th> |
| 3241 | 3403 | <td> |
| 3242 | - <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> | |
| 3243 | 3405 | <p class="description"> |
| 3244 | 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' ); ?> |
| 3245 | 3407 | <br> |
| 3246 | 3408 | <?php |
| @@ -3279,9 +3441,9 @@ | ||
| 3279 | 3441 | <table class="form-table"> |
| 3280 | 3442 | <tr> |
| 3281 | 3443 | <th scope="row"><label for="vigilante-f-firewall-ua-whitelist"><?php esc_html_e( 'User-Agent Whitelist', 'vigilante' ); ?></label></th> |
| 3282 | 3444 | <td> |
| 3283 | - <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> | |
| 3284 | 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> |
| 3285 | 3447 | </td> |
| 3286 | 3448 | </tr> |
| 3287 | 3449 | <tr> |
| @@ -4964,8 +5126,9 @@ | ||
| 4964 | 5126 | <span class="dashicons dashicons-yes-alt"></span> |
| 4965 | 5127 | <p><?php esc_html_e( 'No pending registrations.', 'vigilante' ); ?></p> |
| 4966 | 5128 | </div> |
| 4967 | 5129 | <?php else : ?> |
| 5130 | + <?php $this->render_user_actions_notice(); ?> | |
| 4968 | 5131 | <table class="wp-list-table widefat fixed striped vigilante-pending-users-table"> |
| 4969 | 5132 | <thead> |
| 4970 | 5133 | <tr> |
| 4971 | 5134 | <th><?php esc_html_e( 'User', 'vigilante' ); ?></th> |
| @@ -4994,12 +5157,12 @@ | ||
| 4994 | 5157 | } |
| 4995 | 5158 | ?> |
| 4996 | 5159 | </td> |
| 4997 | 5160 | <td> |
| 4998 | - <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() ); ?>> | |
| 4999 | 5162 | <?php esc_html_e( 'Approve', 'vigilante' ); ?> |
| 5000 | 5163 | </button> |
| 5001 | - <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() ); ?>> | |
| 5002 | 5165 | <?php esc_html_e( 'Reject', 'vigilante' ); ?> |
| 5003 | 5166 | </button> |
| 5004 | 5167 | </td> |
| 5005 | 5168 | </tr> |
| @@ -5893,8 +6056,13 @@ | ||
| 5893 | 6056 | */ |
| 5894 | 6057 | private function render_tab_file_integrity() { |
| 5895 | 6058 | $is_disabled = $this->render_module_disabled_notice( 'file_integrity' ); |
| 5896 | 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(); | |
| 5897 | 6065 | $last_scan = get_option( 'vigilante_last_integrity_scan' ); |
| 5898 | 6066 | $last_results = get_option( 'vigilante_last_integrity_results' ); |
| 5899 | 6067 | $ignored_files = get_option( 'vigilante_ignored_files', array() ); |
| 5900 | 6068 | |
| @@ -6020,10 +6188,13 @@ | ||
| 6020 | 6188 | <?php esc_html_e( 'Uploads directory (detect PHP files, double extensions, .htaccess)', 'vigilante' ); ?> |
| 6021 | 6189 | </label> |
| 6022 | 6190 | <br> |
| 6023 | 6191 | <label> |
| 6024 | - <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 ); ?>> | |
| 6025 | 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; ?> | |
| 6026 | 6197 | </label> |
| 6027 | 6198 | <br> |
| 6028 | 6199 | <label> |
| 6029 | 6200 | <input type="checkbox" name="file_integrity[check_closed_plugins]" value="1" <?php checked( $options['check_closed_plugins'] ?? true ); ?>> |
| @@ -6302,9 +6473,15 @@ | ||
| 6302 | 6473 | $crit_diff = $crit_item['diff'] ?? array(); |
| 6303 | 6474 | $crit_id = sanitize_html_class( $crit_file ); |
| 6304 | 6475 | $added_count = is_array( $crit_diff ) ? count( $crit_diff['added'] ?? array() ) : 0; |
| 6305 | 6476 | $removed_count = is_array( $crit_diff ) ? count( $crit_diff['removed'] ?? array() ) : 0; |
| 6306 | - $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'] ) ); | |
| 6307 | 6484 | ?> |
| 6308 | 6485 | <tr> |
| 6309 | 6486 | <td><code style="color: #e36210;"><?php echo esc_html( $crit_file ); ?></code></td> |
| 6310 | 6487 | <td> |
| @@ -6327,18 +6504,36 @@ | ||
| 6327 | 6504 | <td> |
| 6328 | 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' ); ?>"> |
| 6329 | 6506 | <?php esc_html_e( 'Review changes', 'vigilante' ); ?> |
| 6330 | 6507 | </button> |
| 6331 | - <button type="button" class="button button-small button-primary vigilante-approve-critical-file" data-file="<?php echo esc_attr( $crit_file ); ?>"> | |
| 6332 | - <?php esc_html_e( 'Approve', 'vigilante' ); ?> | |
| 6333 | - </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; ?> | |
| 6334 | 6517 | </td> |
| 6335 | 6518 | </tr> |
| 6336 | 6519 | <tr id="vigilante-critical-content-<?php echo esc_attr( $crit_id ); ?>" class="vigilante-critical-content-row" style="display:none;"> |
| 6337 | 6520 | <td colspan="3" style="padding: 0;"> |
| 6338 | 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;"> |
| 6339 | - <?php if ( $diff_unavailable ) : ?> | |
| 6522 | + <?php if ( $diff_network ) : ?> | |
| 6340 | 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;"> | |
| 6341 | 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' ); ?> |
| 6342 | 6537 | </p> |
| 6343 | 6538 | <?php elseif ( empty( $crit_diff['added'] ) && empty( $crit_diff['removed'] ) ) : ?> |
| 6344 | 6539 | <p style="color: #50575e; font-style: italic; margin: 0;"> |
| @@ -6674,8 +6869,27 @@ | ||
| 6674 | 6869 | |
| 6675 | 6870 | // Read ONLY saved options from database (not merged with defaults) |
| 6676 | 6871 | $saved_options = get_option( Vigilante_Settings::OPTION_NAME, array() ); |
| 6677 | 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 | + | |
| 6678 | 6892 | $rejected_ips = array(); |
| 6679 | 6893 | |
| 6680 | 6894 | // Handle modules |
| 6681 | 6895 | if ( 'modules' === $section && isset( $data['modules'] ) ) { |
| @@ -6714,8 +6928,10 @@ | ||
| 6714 | 6928 | |
| 6715 | 6929 | // Clear cache before saving |
| 6716 | 6930 | wp_cache_delete( Vigilante_Settings::OPTION_NAME, 'options' ); |
| 6717 | 6931 | |
| 6932 | + $saved_options = Vigilante_Settings::keep_locked_file_settings( $saved_options, $stored_options ); | |
| 6933 | + | |
| 6718 | 6934 | // Save to database |
| 6719 | 6935 | update_option( Vigilante_Settings::OPTION_NAME, $saved_options ); |
| 6720 | 6936 | |
| 6721 | 6937 | // Clear the settings cache |
| @@ -7156,8 +7372,9 @@ | ||
| 7156 | 7372 | } |
| 7157 | 7373 | } |
| 7158 | 7374 | |
| 7159 | 7375 | // Save |
| 7376 | + $merged = Vigilante_Settings::keep_locked_file_settings( $merged, get_option( Vigilante_Settings::OPTION_NAME, array() ) ); | |
| 7160 | 7377 | update_option( Vigilante_Settings::OPTION_NAME, $merged ); |
| 7161 | 7378 | $this->settings->clear_cache(); |
| 7162 | 7379 | |
| 7163 | 7380 | // Re-evaluate the active preset marker. The imported config may match |
| @@ -7180,9 +7397,9 @@ | ||
| 7180 | 7397 | if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) { |
| 7181 | 7398 | wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' ); |
| 7182 | 7399 | } |
| 7183 | 7400 | |
| 7184 | - wp_send_json_success( __( 'Settings imported successfully.', 'vigilante' ) ); | |
| 7401 | + wp_send_json_success( __( 'Settings imported successfully.', 'vigilante' ) . $this->locked_file_settings_message() ); | |
| 7185 | 7402 | } |
| 7186 | 7403 | |
| 7187 | 7404 | /** |
| 7188 | 7405 | * Detect whether a vigilante_options array matches a known preset. |
| @@ -7285,9 +7502,11 @@ | ||
| 7285 | 7502 | $preset = isset( $_POST['preset'] ) ? sanitize_key( $_POST['preset'] ) : ''; |
| 7286 | 7503 | |
| 7287 | 7504 | // Handle reset to defaults |
| 7288 | 7505 | if ( 'reset' === $preset ) { |
| 7289 | - $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 ); | |
| 7290 | 7509 | update_option( Vigilante_Settings::OPTION_NAME, $defaults ); |
| 7291 | 7510 | $this->settings->clear_cache(); |
| 7292 | 7511 | |
| 7293 | 7512 | // Clear active preset |
| @@ -7295,9 +7514,9 @@ | ||
| 7295 | 7514 | |
| 7296 | 7515 | // Apply file changes after reset |
| 7297 | 7516 | $this->apply_all_file_changes( $defaults ); |
| 7298 | 7517 | |
| 7299 | - wp_send_json_success( __( 'Settings reset to defaults.', 'vigilante' ) ); | |
| 7518 | + wp_send_json_success( __( 'Settings reset to defaults.', 'vigilante' ) . $this->locked_file_settings_message() ); | |
| 7300 | 7519 | return; |
| 7301 | 7520 | } |
| 7302 | 7521 | |
| 7303 | 7522 | $presets = $this->settings->get_presets(); |
| @@ -7323,8 +7542,9 @@ | ||
| 7323 | 7542 | // invent keys that are missing on both sides. |
| 7324 | 7543 | $current = Vigilante_Settings::merge_preset( $this->settings->get_default_options(), $current ); |
| 7325 | 7544 | |
| 7326 | 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() ) ); | |
| 7327 | 7547 | |
| 7328 | 7548 | update_option( Vigilante_Settings::OPTION_NAME, $merged ); |
| 7329 | 7549 | $this->settings->clear_cache(); |
| 7330 | 7550 | |
| @@ -7333,9 +7553,9 @@ | ||
| 7333 | 7553 | |
| 7334 | 7554 | // Apply file changes after preset |
| 7335 | 7555 | $this->apply_all_file_changes( $merged ); |
| 7336 | 7556 | |
| 7337 | - wp_send_json_success( __( 'Preset applied successfully.', 'vigilante' ) ); | |
| 7557 | + wp_send_json_success( __( 'Preset applied successfully.', 'vigilante' ) . $this->locked_file_settings_message() ); | |
| 7338 | 7558 | } |
| 7339 | 7559 | |
| 7340 | 7560 | /** |
| 7341 | 7561 | * AJAX: Reset a specific section to defaults |
| @@ -7370,27 +7590,19 @@ | ||
| 7370 | 7590 | * On a subsite, the settings written to wp-config.php and .htaccess are |
| 7371 | 7591 | * the main site's business. Resetting the local copy of those would only |
| 7372 | 7592 | * make this screen disagree with the file, so they are carried over |
| 7373 | 7593 | * untouched, and a section that is nothing but shared settings is not |
| 7374 | - * 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). | |
| 7375 | 7596 | */ |
| 7376 | - if ( ! Vigilante_Settings::can_write_shared_files() ) { | |
| 7377 | - $shared = Vigilante_Settings::get_shared_file_settings(); | |
| 7597 | + $locked = Vigilante_Settings::get_locked_file_settings(); | |
| 7378 | 7598 | |
| 7379 | - if ( isset( $shared[ $section ] ) ) { | |
| 7380 | - if ( true === $shared[ $section ] ) { | |
| 7381 | - wp_send_json_error( Vigilante_Settings::get_shared_files_notice() ); | |
| 7382 | - } | |
| 7383 | - | |
| 7384 | - foreach ( $shared[ $section ] as $shared_key ) { | |
| 7385 | - if ( array_key_exists( $shared_key, (array) $current_options[ $section ] ) ) { | |
| 7386 | - $new_values[ $shared_key ] = $current_options[ $section ][ $shared_key ]; | |
| 7387 | - } | |
| 7388 | - } | |
| 7389 | - } | |
| 7599 | + if ( isset( $locked[ $section ] ) && true === $locked[ $section ] ) { | |
| 7600 | + wp_send_json_error( Vigilante_Settings::get_shared_files_notice() ); | |
| 7390 | 7601 | } |
| 7391 | 7602 | |
| 7392 | 7603 | $current_options[ $section ] = $new_values; |
| 7604 | + $current_options = Vigilante_Settings::keep_locked_file_settings( $current_options, get_option( Vigilante_Settings::OPTION_NAME, array() ) ); | |
| 7393 | 7605 | |
| 7394 | 7606 | // Save |
| 7395 | 7607 | update_option( Vigilante_Settings::OPTION_NAME, $current_options ); |
| 7396 | 7608 | $this->settings->clear_cache(); |
| @@ -7473,8 +7685,19 @@ | ||
| 7473 | 7685 | // Save new results |
| 7474 | 7686 | update_option( 'vigilante_last_integrity_scan', time() ); |
| 7475 | 7687 | update_option( 'vigilante_last_integrity_results', $results ); |
| 7476 | 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 | + | |
| 7477 | 7700 | wp_send_json_success( array( |
| 7478 | 7701 | 'message' => __( 'Scan completed.', 'vigilante' ), |
| 7479 | 7702 | 'results' => $results, |
| 7480 | 7703 | 'ignored_count' => count( get_option( 'vigilante_ignored_files', array() ) ), |
| @@ -7508,11 +7731,41 @@ | ||
| 7508 | 7731 | if ( ! current_user_can( 'manage_options' ) ) { |
| 7509 | 7732 | wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 7510 | 7733 | } |
| 7511 | 7734 | |
| 7735 | + $results = get_option( 'vigilante_last_integrity_results' ); | |
| 7736 | + $scanned_at = get_option( 'vigilante_last_integrity_scan' ); | |
| 7737 | + | |
| 7512 | 7738 | delete_option( 'vigilante_last_integrity_results' ); |
| 7513 | 7739 | delete_option( 'vigilante_last_integrity_scan' ); |
| 7514 | 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 | + | |
| 7515 | 7768 | if ( $this->database ) { |
| 7516 | 7769 | $this->database->clear_file_hashes(); |
| 7517 | 7770 | } |
| 7518 | 7771 | |
| @@ -7538,8 +7791,14 @@ | ||
| 7538 | 7791 | if ( empty( $file ) ) { |
| 7539 | 7792 | wp_send_json_error( __( 'No file specified.', 'vigilante' ) ); |
| 7540 | 7793 | } |
| 7541 | 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 | + | |
| 7542 | 7801 | $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database ); |
| 7543 | 7802 | $file_integrity->ignore_file( $file ); |
| 7544 | 7803 | |
| 7545 | 7804 | // Also remove the file from stored scan results so UI updates |
| @@ -7603,12 +7862,14 @@ | ||
| 7603 | 7862 | if ( ! is_array( $raw_files ) ) { |
| 7604 | 7863 | wp_send_json_error( __( 'Invalid request.', 'vigilante' ) ); |
| 7605 | 7864 | } |
| 7606 | 7865 | |
| 7607 | - $files = array(); | |
| 7866 | + $files = array(); | |
| 7867 | + $shared = $this->critical_approval_locked() ? array( 'wp-config.php', '.htaccess' ) : array(); | |
| 7608 | 7868 | foreach ( $raw_files as $f ) { |
| 7609 | 7869 | $clean = sanitize_text_field( $f ); |
| 7610 | - if ( '' !== $clean ) { | |
| 7870 | + // Same rule as ajax_ignore_file() for the two shared files. | |
| 7871 | + if ( '' !== $clean && ! in_array( $clean, $shared, true ) ) { | |
| 7611 | 7872 | $files[] = $clean; |
| 7612 | 7873 | } |
| 7613 | 7874 | } |
| 7614 | 7875 | |