| @@ -1,11 +1,11 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | 3 | * Backup Manager Class |
| 4 | 4 | * |
| 5 | - * Handles backup and restoration of critical files. Backups are stored in | |
| 6 | - * private database options (autoload off), never as files under the web root, | |
| 7 | - * so a copy of wp-config.php or .htaccess can never be served over HTTP. | |
| 5 | + * Builds the downloadable archive of the configuration files, and cleans up | |
| 6 | + * the copies of those files that earlier versions kept: on disk under the web | |
| 7 | + * root until 2.7.0, and in the options table until 2.11.6. | |
| 8 | 8 | * |
| 9 | 9 | * @package Vigilante |
| 10 | 10 | */ |
| 11 | 11 | |
| @@ -16,29 +16,54 @@ | ||
| 16 | 16 | |
| 17 | 17 | /** |
| 18 | 18 | * Class Vigilante_Backup_Manager |
| 19 | 19 | * |
| 20 | - * Manages file backups for security modifications. | |
| 20 | + * Configuration file archive and cleanup of stored copies. | |
| 21 | 21 | */ |
| 22 | 22 | class Vigilante_Backup_Manager { |
| 23 | 23 | |
| 24 | 24 | /** |
| 25 | - * Legacy on-disk backup directory (kept only to clean it up on upgrade). | |
| 25 | + * Option recording that the stored copies are gone | |
| 26 | 26 | * |
| 27 | - * @var string | |
| 27 | + * 1 once this site is clean, 2 once the walk over the network has finished | |
| 28 | + * as well, so the site stops asking the network. Autoloaded, because it is | |
| 29 | + * read on every request. | |
| 30 | + * | |
| 31 | + * @since 2.11.6 | |
| 28 | 32 | */ |
| 29 | - private $backup_dir; | |
| 33 | + const COPIES_PURGED_OPTION = 'vigilante_config_copies_purged'; | |
| 30 | 34 | |
| 31 | 35 | /** |
| 32 | - * Maximum number of backups to keep | |
| 36 | + * Network option holding the last site the walk cleaned, or 'done' | |
| 33 | 37 | * |
| 34 | - * @var int | |
| 38 | + * @since 2.11.6 | |
| 35 | 39 | */ |
| 36 | - private $max_backups = 5; | |
| 40 | + const COPIES_SWEEP_OPTION = 'vigilante_config_copies_sweep'; | |
| 37 | 41 | |
| 38 | 42 | /** |
| 39 | - * Files to backup | |
| 43 | + * Sites the walk cleans per request | |
| 40 | 44 | * |
| 45 | + * @since 2.11.6 | |
| 46 | + */ | |
| 47 | + const COPIES_SWEEP_BATCH = 50; | |
| 48 | + | |
| 49 | + /** | |
| 50 | + * Lock that keeps two requests from walking the network at once | |
| 51 | + * | |
| 52 | + * @since 2.11.8 | |
| 53 | + */ | |
| 54 | + const COPIES_SWEEP_LOCK = 'vigilante_config_copies_sweep_lock'; | |
| 55 | + | |
| 56 | + /** | |
| 57 | + * Legacy on-disk backup directory (kept only to clean it up on upgrade). | |
| 58 | + * | |
| 59 | + * @var string | |
| 60 | + */ | |
| 61 | + private $backup_dir; | |
| 62 | + | |
| 63 | + /** | |
| 64 | + * Configuration files the archive carries | |
| 65 | + * | |
| 41 | 66 | * @var array |
| 42 | 67 | */ |
| 43 | 68 | private $backup_files = array(); |
| 44 | 69 | |
| @@ -70,221 +95,131 @@ | ||
| 70 | 95 | ); |
| 71 | 96 | } |
| 72 | 97 | |
| 73 | 98 | /** |
| 74 | - * Create backups of all important files | |
| 99 | + * Delete the copies of the configuration files kept in this site's options | |
| 75 | 100 | * |
| 76 | - * The content is stored in the database, never copied to a file under the | |
| 77 | - * web root. | |
| 101 | + * Until 2.11.6 activating Vigilant copied wp-config.php, .htaccess and | |
| 102 | + * robots.txt into vigilante_backup_info_<date>, up to five of them, and | |
| 103 | + * writing the constants block kept one more copy of wp-config.php in | |
| 104 | + * vigilante_wpconfig_backup. Nothing read them back: the method that | |
| 105 | + * restored the files had no caller. wp-config.php carries the database | |
| 106 | + * password and the authentication keys and salts, so every copy put them in | |
| 107 | + * the options table, within reach of anyone who can read the database or a | |
| 108 | + * dump of it. 2.7.0 moved the copies there from files under the web root, | |
| 109 | + * which changed where they lived and not whether they should exist. | |
| 78 | 110 | * |
| 79 | - * @return true|WP_Error True on success, WP_Error on failure. | |
| 111 | + * @since 2.11.6 | |
| 80 | 112 | */ |
| 81 | - public function create_backups() { | |
| 82 | - $timestamp = gmdate( 'Y-m-d_H-i-s' ); | |
| 83 | - $backup_info = array(); | |
| 84 | - $errors = array(); | |
| 113 | + public static function purge_stored_copies() { | |
| 114 | + global $wpdb; | |
| 85 | 115 | |
| 86 | - foreach ( $this->backup_files as $key => $file ) { | |
| 87 | - if ( file_exists( $file['source'] ) ) { | |
| 88 | - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_get_contents -- reading a known local config file to store it in the DB, not a filesystem op on user input. | |
| 89 | - $content = file_get_contents( $file['source'] ); | |
| 116 | + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- one-off cleanup of our own dated option names, which the options API cannot list. | |
| 117 | + $names = $wpdb->get_col( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'vigilante\\_backup\\_info\\_%'" ); | |
| 90 | 118 | |
| 91 | - if ( false !== $content ) { | |
| 92 | - $backup_info[ $key ] = array( | |
| 93 | - 'content' => $content, | |
| 94 | - 'hash' => md5( $content ), | |
| 95 | - 'size' => strlen( $content ), | |
| 96 | - 'exists' => true, | |
| 97 | - 'time' => time(), | |
| 98 | - ); | |
| 99 | - } else { | |
| 100 | - $errors[] = sprintf( | |
| 101 | - /* translators: %s: File name */ | |
| 102 | - __( 'Failed to backup %s', 'vigilante' ), | |
| 103 | - basename( $file['source'] ) | |
| 104 | - ); | |
| 105 | - } | |
| 106 | - } else { | |
| 107 | - // Mark as non-existent (important for restoration). | |
| 108 | - $backup_info[ $key ] = array( | |
| 109 | - 'content' => '', | |
| 110 | - 'exists' => false, | |
| 111 | - 'time' => time(), | |
| 112 | - ); | |
| 113 | - } | |
| 119 | + foreach ( (array) $names as $name ) { | |
| 120 | + delete_option( $name ); | |
| 114 | 121 | } |
| 115 | 122 | |
| 116 | - if ( ! empty( $errors ) ) { | |
| 117 | - return new WP_Error( 'backup_partial', implode( ', ', $errors ) ); | |
| 118 | - } | |
| 119 | - | |
| 120 | - // Store metadata + content in non-autoloaded options (may be large and | |
| 121 | - // is only needed on demand). | |
| 122 | - $backup_info['timestamp'] = $timestamp; | |
| 123 | - update_option( 'vigilante_backup_timestamp', $timestamp, false ); | |
| 124 | - update_option( 'vigilante_backup_info_' . $timestamp, $backup_info, false ); | |
| 125 | - | |
| 126 | - $this->cleanup_old_backups(); | |
| 127 | - | |
| 128 | - return true; | |
| 123 | + delete_option( 'vigilante_backup_timestamp' ); | |
| 124 | + delete_option( 'vigilante_wpconfig_backup' ); | |
| 129 | 125 | } |
| 130 | 126 | |
| 131 | 127 | /** |
| 132 | - * Restore files from backup | |
| 128 | + * Run purge_stored_copies() once on this site, and once over the network | |
| 133 | 129 | * |
| 134 | - * @param string $timestamp Optional specific timestamp to restore. | |
| 135 | - * @return true|WP_Error | |
| 130 | + * The walk exists for the sites where Vigilant no longer runs: a copy made | |
| 131 | + * by a per-site activation stays in that site's options after the plugin is | |
| 132 | + * deactivated there, and nothing on that site would ever clean it. It | |
| 133 | + * advances one batch of sites per request, from whichever site runs | |
| 134 | + * Vigilant, remembers the last site it cleaned, and only visits this | |
| 135 | + * network. | |
| 136 | + * | |
| 137 | + * @since 2.11.6 | |
| 136 | 138 | */ |
| 137 | - public function restore_backups( $timestamp = '' ) { | |
| 138 | - if ( empty( $timestamp ) ) { | |
| 139 | - $timestamp = get_option( 'vigilante_backup_timestamp' ); | |
| 139 | + public static function maybe_purge_stored_copies() { | |
| 140 | + $state = (int) get_option( self::COPIES_PURGED_OPTION, 0 ); | |
| 141 | + | |
| 142 | + if ( $state >= 2 ) { | |
| 143 | + return; | |
| 140 | 144 | } |
| 141 | 145 | |
| 142 | - if ( empty( $timestamp ) ) { | |
| 143 | - return new WP_Error( | |
| 144 | - 'no_backup', | |
| 145 | - __( 'No backup found to restore.', 'vigilante' ) | |
| 146 | - ); | |
| 146 | + if ( $state < 1 ) { | |
| 147 | + self::purge_stored_copies(); | |
| 148 | + update_option( self::COPIES_PURGED_OPTION, 1, true ); | |
| 147 | 149 | } |
| 148 | 150 | |
| 149 | - $backup_info = get_option( 'vigilante_backup_info_' . $timestamp ); | |
| 150 | - | |
| 151 | - if ( empty( $backup_info ) ) { | |
| 152 | - return new WP_Error( | |
| 153 | - 'backup_info_missing', | |
| 154 | - __( 'Backup information not found.', 'vigilante' ) | |
| 155 | - ); | |
| 151 | + if ( ! is_multisite() ) { | |
| 152 | + update_option( self::COPIES_PURGED_OPTION, 2, true ); | |
| 153 | + return; | |
| 156 | 154 | } |
| 157 | 155 | |
| 158 | - $errors = array(); | |
| 156 | + if ( 'done' !== get_site_option( self::COPIES_SWEEP_OPTION, 0 ) ) { | |
| 157 | + /* | |
| 158 | + * One request walks at a time. Right after an update every request | |
| 159 | + * gets here, and until 2.11.8 each of them repeated the same batch of | |
| 160 | + * sites. The lock lives in the options table of the main site, which | |
| 161 | + * every site of the network reaches the same way. | |
| 162 | + */ | |
| 163 | + switch_to_blog( get_main_site_id() ); | |
| 164 | + $locked = Vigilante_Settings::acquire_option_lock( self::COPIES_SWEEP_LOCK, MINUTE_IN_SECONDS ); | |
| 165 | + restore_current_blog(); | |
| 159 | 166 | |
| 160 | - foreach ( $this->backup_files as $key => $file ) { | |
| 161 | - if ( ! isset( $backup_info[ $key ] ) ) { | |
| 162 | - continue; | |
| 167 | + if ( ! $locked ) { | |
| 168 | + return; | |
| 163 | 169 | } |
| 164 | 170 | |
| 165 | - $info = $backup_info[ $key ]; | |
| 166 | - | |
| 167 | - // If the file did not exist originally, delete it. | |
| 168 | - if ( isset( $info['exists'] ) && false === $info['exists'] ) { | |
| 169 | - if ( file_exists( $file['source'] ) ) { | |
| 170 | - wp_delete_file( $file['source'] ); | |
| 171 | - } | |
| 172 | - continue; | |
| 171 | + try { | |
| 172 | + self::sweep_next_batch(); | |
| 173 | + } finally { | |
| 174 | + switch_to_blog( get_main_site_id() ); | |
| 175 | + Vigilante_Settings::release_option_lock( self::COPIES_SWEEP_LOCK ); | |
| 176 | + restore_current_blog(); | |
| 173 | 177 | } |
| 174 | - | |
| 175 | - if ( ! isset( $info['content'] ) || '' === $info['content'] ) { | |
| 176 | - continue; | |
| 177 | - } | |
| 178 | - | |
| 179 | - // Verify integrity against the stored hash. | |
| 180 | - if ( isset( $info['hash'] ) && md5( $info['content'] ) !== $info['hash'] ) { | |
| 181 | - $errors[] = sprintf( | |
| 182 | - /* translators: %s: File name */ | |
| 183 | - __( 'Backup integrity check failed for %s', 'vigilante' ), | |
| 184 | - $file['name'] | |
| 185 | - ); | |
| 186 | - continue; | |
| 187 | - } | |
| 188 | - | |
| 189 | - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- restoring a known local config file from the DB backup. | |
| 190 | - if ( false === file_put_contents( $file['source'], $info['content'] ) ) { | |
| 191 | - $errors[] = sprintf( | |
| 192 | - /* translators: %s: File name */ | |
| 193 | - __( 'Failed to restore %s', 'vigilante' ), | |
| 194 | - basename( $file['source'] ) | |
| 195 | - ); | |
| 196 | - } | |
| 197 | 178 | } |
| 198 | 179 | |
| 199 | - if ( ! empty( $errors ) ) { | |
| 200 | - return new WP_Error( 'restore_partial', implode( ', ', $errors ) ); | |
| 180 | + if ( 'done' === get_site_option( self::COPIES_SWEEP_OPTION, 0 ) ) { | |
| 181 | + update_option( self::COPIES_PURGED_OPTION, 2, true ); | |
| 201 | 182 | } |
| 202 | - | |
| 203 | - return true; | |
| 204 | 183 | } |
| 205 | 184 | |
| 206 | 185 | /** |
| 207 | - * Cleanup old backups keeping only the most recent | |
| 186 | + * Clean the next batch of sites of the network, with the walk lock held | |
| 187 | + * | |
| 188 | + * @since 2.11.8 | |
| 208 | 189 | */ |
| 209 | - private function cleanup_old_backups() { | |
| 210 | - $settings = new Vigilante_Settings(); | |
| 211 | - $backup_settings = $settings->get_section( 'backup' ); | |
| 212 | - // The default is stored as keep_backups. Until 2.9.9 this read max_backups, | |
| 213 | - // a key nothing ever wrote, so the configured value was ignored and the | |
| 214 | - // hardcoded 5 always won. | |
| 215 | - $this->max_backups = isset( $backup_settings['keep_backups'] ) ? absint( $backup_settings['keep_backups'] ) : 5; | |
| 216 | - | |
| 190 | + private static function sweep_next_batch() { | |
| 217 | 191 | global $wpdb; |
| 218 | 192 | |
| 219 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- one-off maintenance scan of our own option names. | |
| 220 | - $backup_options = $wpdb->get_col( | |
| 221 | - "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'vigilante_backup_info_%' ORDER BY option_name DESC" | |
| 222 | - ); | |
| 193 | + // Read again inside the lock: the request that held it before may have | |
| 194 | + // moved the walk on. | |
| 195 | + // The core caches network options under "$network_id:$option", and absent | |
| 196 | + // ones in "$network_id:notoptions" (wp-includes/option.php:2091 and :2065). | |
| 197 | + wp_cache_delete( get_current_network_id() . ':' . self::COPIES_SWEEP_OPTION, 'site-options' ); | |
| 198 | + wp_cache_delete( get_current_network_id() . ':notoptions', 'site-options' ); | |
| 199 | + $sweep = get_site_option( self::COPIES_SWEEP_OPTION, 0 ); | |
| 223 | 200 | |
| 224 | - if ( count( $backup_options ) > $this->max_backups ) { | |
| 225 | - $to_delete = array_slice( $backup_options, $this->max_backups ); | |
| 226 | - | |
| 227 | - foreach ( $to_delete as $option_name ) { | |
| 228 | - $timestamp = str_replace( 'vigilante_backup_info_', '', $option_name ); | |
| 229 | - $this->delete_backup( $timestamp ); | |
| 230 | - } | |
| 201 | + if ( 'done' === $sweep ) { | |
| 202 | + return; | |
| 231 | 203 | } |
| 232 | - } | |
| 233 | 204 | |
| 234 | - /** | |
| 235 | - * Delete a specific backup | |
| 236 | - * | |
| 237 | - * @param string $timestamp Backup timestamp. | |
| 238 | - * @return bool | |
| 239 | - */ | |
| 240 | - public function delete_backup( $timestamp ) { | |
| 241 | - delete_option( 'vigilante_backup_info_' . $timestamp ); | |
| 242 | - | |
| 243 | - $current_timestamp = get_option( 'vigilante_backup_timestamp' ); | |
| 244 | - if ( $current_timestamp === $timestamp ) { | |
| 245 | - delete_option( 'vigilante_backup_timestamp' ); | |
| 246 | - } | |
| 247 | - | |
| 248 | - return true; | |
| 249 | - } | |
| 250 | - | |
| 251 | - /** | |
| 252 | - * Get list of available backups | |
| 253 | - * | |
| 254 | - * @return array | |
| 255 | - */ | |
| 256 | - public function get_available_backups() { | |
| 257 | - global $wpdb; | |
| 258 | - | |
| 259 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- listing our own option names. | |
| 260 | - $backup_options = $wpdb->get_col( | |
| 261 | - "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'vigilante_backup_info_%' ORDER BY option_name DESC" | |
| 205 | + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- the walk needs the sites after the last one it cleaned, in id order, and get_sites() cannot ask for ids greater than a value. | |
| 206 | + $site_ids = $wpdb->get_col( | |
| 207 | + $wpdb->prepare( | |
| 208 | + "SELECT blog_id FROM {$wpdb->blogs} WHERE site_id = %d AND blog_id > %d ORDER BY blog_id ASC LIMIT %d", | |
| 209 | + get_current_network_id(), | |
| 210 | + (int) $sweep, | |
| 211 | + self::COPIES_SWEEP_BATCH | |
| 212 | + ) | |
| 262 | 213 | ); |
| 263 | 214 | |
| 264 | - $backups = array(); | |
| 265 | - | |
| 266 | - foreach ( $backup_options as $option_name ) { | |
| 267 | - $timestamp = str_replace( 'vigilante_backup_info_', '', $option_name ); | |
| 268 | - $info = get_option( $option_name ); | |
| 269 | - | |
| 270 | - if ( ! empty( $info ) ) { | |
| 271 | - $backups[] = array( | |
| 272 | - 'timestamp' => $timestamp, | |
| 273 | - 'date' => date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( str_replace( '_', ' ', $timestamp ) ) ), | |
| 274 | - 'files' => count( | |
| 275 | - array_filter( | |
| 276 | - $info, | |
| 277 | - function ( $item ) { | |
| 278 | - return is_array( $item ) && ! empty( $item['exists'] ); | |
| 279 | - } | |
| 280 | - ) | |
| 281 | - ), | |
| 282 | - ); | |
| 283 | - } | |
| 215 | + foreach ( $site_ids as $site_id ) { | |
| 216 | + switch_to_blog( (int) $site_id ); | |
| 217 | + self::purge_stored_copies(); | |
| 218 | + restore_current_blog(); | |
| 284 | 219 | } |
| 285 | 220 | |
| 286 | - return $backups; | |
| 221 | + update_site_option( self::COPIES_SWEEP_OPTION, count( $site_ids ) < self::COPIES_SWEEP_BATCH ? 'done' : (int) end( $site_ids ) ); | |
| 287 | 222 | } |
| 288 | 223 | |
| 289 | 224 | /** |
| 290 | 225 | * Get the legacy on-disk backup directory path. |
| @@ -298,27 +233,8 @@ | ||
| 298 | 233 | return $this->backup_dir; |
| 299 | 234 | } |
| 300 | 235 | |
| 301 | 236 | /** |
| 302 | - * Check if backups exist | |
| 303 | - * | |
| 304 | - * @return bool | |
| 305 | - */ | |
| 306 | - public function has_backups() { | |
| 307 | - $timestamp = get_option( 'vigilante_backup_timestamp' ); | |
| 308 | - return ! empty( $timestamp ); | |
| 309 | - } | |
| 310 | - | |
| 311 | - /** | |
| 312 | - * Get last backup timestamp | |
| 313 | - * | |
| 314 | - * @return string|false | |
| 315 | - */ | |
| 316 | - public function get_last_backup_timestamp() { | |
| 317 | - return get_option( 'vigilante_backup_timestamp' ); | |
| 318 | - } | |
| 319 | - | |
| 320 | - /** | |
| 321 | 237 | * Build a ZIP with the current config files and stream it to the browser. |
| 322 | 238 | * |
| 323 | 239 | * Used by the "Create Backup" tool: instead of leaving files under the web |
| 324 | 240 | * root, it hands the admin a downloadable archive of wp-config.php and |
| @@ -326,8 +242,14 @@ | ||
| 326 | 242 | * |
| 327 | 243 | * @return void|WP_Error WP_Error on failure; on success it streams and exits. |
| 328 | 244 | */ |
| 329 | 245 | public function stream_files_zip() { |
| 246 | + // Second line of defence: the archive contains network-shared files, so | |
| 247 | + // no future caller can hand them to a subsite administrator by mistake. | |
| 248 | + if ( ! Vigilante_Settings::can_write_shared_files() ) { | |
| 249 | + return new WP_Error( 'shared_files_denied', Vigilante_Settings::get_shared_files_notice() ); | |
| 250 | + } | |
| 251 | + | |
| 330 | 252 | if ( ! class_exists( 'ZipArchive' ) ) { |
| 331 | 253 | return new WP_Error( 'zip_unavailable', __( 'ZipArchive extension is not available on this server.', 'vigilante' ) ); |
| 332 | 254 | } |
| 333 | 255 | |
| @@ -375,78 +297,8 @@ | ||
| 375 | 297 | // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- streaming a freshly built archive to the browser. |
| 376 | 298 | readfile( $zip_path ); |
| 377 | 299 | wp_delete_file( $zip_path ); |
| 378 | 300 | exit; |
| 379 | - } | |
| 380 | - | |
| 381 | - /** | |
| 382 | - * Verify backup integrity | |
| 383 | - * | |
| 384 | - * @param string $timestamp Backup timestamp. | |
| 385 | - * @return array Verification results. | |
| 386 | - */ | |
| 387 | - public function verify_backup( $timestamp ) { | |
| 388 | - $backup_info = get_option( 'vigilante_backup_info_' . $timestamp ); | |
| 389 | - | |
| 390 | - if ( empty( $backup_info ) ) { | |
| 391 | - return array( | |
| 392 | - 'valid' => false, | |
| 393 | - 'errors' => array( __( 'Backup information not found.', 'vigilante' ) ), | |
| 394 | - ); | |
| 395 | - } | |
| 396 | - | |
| 397 | - $results = array( | |
| 398 | - 'valid' => true, | |
| 399 | - 'errors' => array(), | |
| 400 | - 'files' => array(), | |
| 401 | - ); | |
| 402 | - | |
| 403 | - foreach ( $this->backup_files as $key => $file ) { | |
| 404 | - if ( ! isset( $backup_info[ $key ] ) ) { | |
| 405 | - continue; | |
| 406 | - } | |
| 407 | - | |
| 408 | - $info = $backup_info[ $key ]; | |
| 409 | - | |
| 410 | - // Skip non-existent files. | |
| 411 | - if ( isset( $info['exists'] ) && false === $info['exists'] ) { | |
| 412 | - $results['files'][ $key ] = array( | |
| 413 | - 'status' => 'skipped', | |
| 414 | - 'reason' => __( 'File did not exist', 'vigilante' ), | |
| 415 | - ); | |
| 416 | - continue; | |
| 417 | - } | |
| 418 | - | |
| 419 | - if ( ! isset( $info['content'] ) ) { | |
| 420 | - $results['valid'] = false; | |
| 421 | - $results['errors'][] = sprintf( | |
| 422 | - /* translators: %s: File name */ | |
| 423 | - __( 'Backup content missing: %s', 'vigilante' ), | |
| 424 | - $file['name'] | |
| 425 | - ); | |
| 426 | - $results['files'][ $key ] = array( 'status' => 'missing' ); | |
| 427 | - continue; | |
| 428 | - } | |
| 429 | - | |
| 430 | - // Verify hash. | |
| 431 | - if ( isset( $info['hash'] ) && md5( $info['content'] ) !== $info['hash'] ) { | |
| 432 | - $results['valid'] = false; | |
| 433 | - $results['errors'][] = sprintf( | |
| 434 | - /* translators: %s: File name */ | |
| 435 | - __( 'Backup corrupted: %s', 'vigilante' ), | |
| 436 | - $file['name'] | |
| 437 | - ); | |
| 438 | - $results['files'][ $key ] = array( 'status' => 'corrupted' ); | |
| 439 | - continue; | |
| 440 | - } | |
| 441 | - | |
| 442 | - $results['files'][ $key ] = array( | |
| 443 | - 'status' => 'valid', | |
| 444 | - 'size' => isset( $info['size'] ) ? (int) $info['size'] : strlen( $info['content'] ), | |
| 445 | - ); | |
| 446 | - } | |
| 447 | - | |
| 448 | - return $results; | |
| 449 | 301 | } |
| 450 | 302 | |
| 451 | 303 | /** |
| 452 | 304 | * Remove backup files written under the web root by versions before 2.7.0. |