From 6737d0a202d60200d8eed4a10fae7d9ade63d347 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Sat, 21 Sep 2013 06:49:11 +0000 Subject: [PATCH] Upgrader: Perform a MD5 file verification check on the files during upgrade. This ensures that both a Partial upgrade build can be used, and that all the files were copied into place correctly. Props pento for initial patch. Fixes #18201 Built from https://develop.svn.wordpress.org/trunk@25540 git-svn-id: http://core.svn.wordpress.org/trunk@25460 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-admin/includes/class-wp-upgrader.php | 24 +++++++- wp-admin/includes/file.php | 22 +++---- wp-admin/includes/update-core.php | 77 ++++++++++++++++++++----- wp-admin/includes/update.php | 62 ++++++++++++++++++++ 4 files changed, 159 insertions(+), 26 deletions(-) diff --git a/wp-admin/includes/class-wp-upgrader.php b/wp-admin/includes/class-wp-upgrader.php index 30c63c06c7..5fb67a2061 100644 --- a/wp-admin/includes/class-wp-upgrader.php +++ b/wp-admin/includes/class-wp-upgrader.php @@ -1111,11 +1111,18 @@ class Core_Upgrader extends WP_Upgrader { $wp_dir = trailingslashit($wp_filesystem->abspath()); + // Pre-cache the checksums for the versions we care about + get_core_checksums( array( $wp_version, $current->version ) ); + + $no_partial = false; + if ( ! $this->check_files() ) + $no_partial = true; + // If partial update is returned from the API, use that, unless we're doing a reinstall. // If we cross the new_bundled version number, then use the new_bundled zip. // Don't though if the constant is set to skip bundled items. // If the API returns a no_content zip, go with it. Finally, default to the full zip. - if ( $current->packages->partial && 'reinstall' != $current->response && $wp_version == $current->partial_version ) + if ( $current->packages->partial && 'reinstall' != $current->response && $wp_version == $current->partial_version && ! $no_partial ) $to_download = 'partial'; elseif ( $current->packages->new_bundled && version_compare( $wp_version, $current->new_bundled, '<' ) && ( ! defined( 'CORE_UPGRADE_SKIP_NEW_BUNDLED' ) || ! CORE_UPGRADE_SKIP_NEW_BUNDLED ) ) @@ -1205,6 +1212,21 @@ class Core_Upgrader extends WP_Upgrader { return false; } + function check_files() { + global $wp_version; + + $checksums = get_core_checksums( $wp_version ); + + if ( empty( $checksums[ $wp_version ] ) || ! is_array( $checksums[ $wp_version ] ) ) + return false; + + foreach ( $checksums[ $wp_version ] as $file => $checksum ) { + if ( md5_file( ABSPATH . $file ) !== $checksum ) + return false; + } + + return true; + } } /** diff --git a/wp-admin/includes/file.php b/wp-admin/includes/file.php index b4ee8ec105..c731cb8f0a 100644 --- a/wp-admin/includes/file.php +++ b/wp-admin/includes/file.php @@ -723,17 +723,9 @@ function copy_dir($from, $to, $skip_list = array() ) { $from = trailingslashit($from); $to = trailingslashit($to); - $skip_regex = ''; - foreach ( (array)$skip_list as $key => $skip_file ) - $skip_regex .= preg_quote($skip_file, '!') . '|'; - - if ( !empty($skip_regex) ) - $skip_regex = '!(' . rtrim($skip_regex, '|') . ')$!i'; - foreach ( (array) $dirlist as $filename => $fileinfo ) { - if ( !empty($skip_regex) ) - if ( preg_match($skip_regex, $from . $filename) ) - continue; + if ( in_array( $filename, $skip_list ) ) + continue; if ( 'f' == $fileinfo['type'] ) { if ( ! $wp_filesystem->copy($from . $filename, $to . $filename, true, FS_CHMOD_FILE) ) { @@ -747,7 +739,15 @@ function copy_dir($from, $to, $skip_list = array() ) { if ( !$wp_filesystem->mkdir($to . $filename, FS_CHMOD_DIR) ) return new WP_Error('mkdir_failed', __('Could not create directory.'), $to . $filename); } - $result = copy_dir($from . $filename, $to . $filename, $skip_list); + + // generate the $sub_skip_list for the subdirectory as a sub-set of the existing $skip_list + $sub_skip_list = array(); + foreach ( $skip_list as $skip_item ) { + if ( 0 === strpos( $skip_item, $filename . '/' ) ) + $sub_skip_list[] = preg_replace( '!^' . preg_quote( $filename, '!' ) . '/!i', '', $skip_item ); + } + + $result = copy_dir($from . $filename, $to . $filename, $sub_skip_list); if ( is_wp_error($result) ) return $result; } diff --git a/wp-admin/includes/update-core.php b/wp-admin/includes/update-core.php index a97f6a004f..a4eecf4f92 100644 --- a/wp-admin/includes/update-core.php +++ b/wp-admin/includes/update-core.php @@ -688,16 +688,62 @@ function update_core($from, $to) { elseif ( !$mysql_compat ) return new WP_Error( 'mysql_not_compatible', sprintf( __('The update cannot be installed because WordPress %1$s requires MySQL version %2$s or higher. You are running version %3$s.'), $wp_version, $required_mysql_version, $mysql_version ) ); - apply_filters('update_feedback', __('Installing the latest version…')); + apply_filters( 'update_feedback', __( 'Preparing to install the latest version…' ) ); + // Don't copy wp-content, we'll deal with that below + $skip = array( 'wp-content' ); + + // Check to see which files don't really need updating - only available for 3.7 and higher + if ( function_exists( 'get_core_checksums' ) ) { + $checksums = get_core_checksums( $wp_version ); + if ( ! empty( $checksums[ $wp_version ] ) && is_array( $checksums[ $wp_version ] ) ) { + foreach( $checksums[ $wp_version ] as $file => $checksum ) { + if ( md5_file( ABSPATH . $file ) === $checksum ) + $skip[] = $file; + } + } + } + + apply_filters( 'update_feedback', __( 'Enabling Maintenance mode…' ) ); // Create maintenance file to signal that we are upgrading $maintenance_string = ''; $maintenance_file = $to . '.maintenance'; $wp_filesystem->delete($maintenance_file); $wp_filesystem->put_contents($maintenance_file, $maintenance_string, FS_CHMOD_FILE); + apply_filters( 'update_feedback', __( 'Copying the required files…' ) ); // Copy new versions of WP files into place. - $result = _copy_dir($from . $distro, $to, array('wp-content') ); + $result = _copy_dir( $from . $distro, $to, $skip ); + + // Check to make sure everything copied correctly, ignoring the contents of wp-content + $skip = array( 'wp-content' ); + $failed = array(); + if ( ! empty( $checksums[ $wp_version ] ) && is_array( $checksums[ $wp_version ] ) ) { + foreach ( $checksums[ $wp_version ] as $file => $checksum ) { + if ( 0 === strpos( $file, 'wp-content' ) ) + continue; + + if ( md5_file( ABSPATH . $file ) == $checksum ) + $skip[] = $file; + else + $failed[] = $file; + } + } + + // Some files didn't copy properly + if ( ! empty( $failed ) ) { + $total_size = 0; + // Find the local version of the working directory + $working_dir_local = str_replace( trailingslashit( $wp_filesystem->wp_content_dir() ), trailingslashit( WP_CONTENT_DIR ), $from . $distro ); + foreach ( $failed as $file ) + $total_size += filesize( $working_dir_local . '/' . $file ); + + // If we don't have enough free space, it isn't worth trying again + if ( $total_size >= disk_free_space( ABSPATH ) ) + $result = new WP_Error( 'disk_full', __( "There isn't enough free disk space to complete the upgrade." ), $to ); + else + $result = _copy_dir( $from . $distro, $to, $skip ); + } // Custom Content Directory needs updating now. // Copy Languages @@ -795,6 +841,7 @@ function update_core($from, $to) { else delete_option('update_core'); + apply_filters( 'update_feedback', __( 'Disabling Maintenance mode…' ) ); // Remove maintenance file, we're done. $wp_filesystem->delete($maintenance_file); @@ -808,10 +855,12 @@ function update_core($from, $to) { * Copies a directory from one location to another via the WordPress Filesystem Abstraction. * Assumes that WP_Filesystem() has already been called and setup. * - * This is a temporary function for the 3.1 -> 3.2 upgrade only and will be removed in 3.3 + * This is a temporary function for the 3.1 -> 3.2 upgrade, as well as for those upgrading to + * 3.7+ * * @ignore * @since 3.2.0 + * @since 3.7.0 Updated not to use a regular expression for the skip list * @see copy_dir() * * @param string $from source directory @@ -827,17 +876,9 @@ function _copy_dir($from, $to, $skip_list = array() ) { $from = trailingslashit($from); $to = trailingslashit($to); - $skip_regex = ''; - foreach ( (array)$skip_list as $key => $skip_file ) - $skip_regex .= preg_quote($skip_file, '!') . '|'; - - if ( !empty($skip_regex) ) - $skip_regex = '!(' . rtrim($skip_regex, '|') . ')$!i'; - foreach ( (array) $dirlist as $filename => $fileinfo ) { - if ( !empty($skip_regex) ) - if ( preg_match($skip_regex, $from . $filename) ) - continue; + if ( in_array( $filename, $skip_list ) ) + continue; if ( 'f' == $fileinfo['type'] ) { if ( ! $wp_filesystem->copy($from . $filename, $to . $filename, true, FS_CHMOD_FILE) ) { @@ -851,7 +892,15 @@ function _copy_dir($from, $to, $skip_list = array() ) { if ( !$wp_filesystem->mkdir($to . $filename, FS_CHMOD_DIR) ) return new WP_Error('mkdir_failed', __('Could not create directory.'), $to . $filename); } - $result = _copy_dir($from . $filename, $to . $filename, $skip_list); + + // generate the $sub_skip_list for the subdirectory as a sub-set of the existing $skip_list + $sub_skip_list = array(); + foreach ( $skip_list as $skip_item ) { + if ( 0 === strpos( $skip_item, $filename . '/' ) ) + $sub_skip_list[] = preg_replace( '!^' . preg_quote( $filename, '!' ) . '/!i', '', $skip_item ); + } + + $result = _copy_dir($from . $filename, $to . $filename, $sub_skip_list); if ( is_wp_error($result) ) return $result; } diff --git a/wp-admin/includes/update.php b/wp-admin/includes/update.php index dc1f5b41f9..9417c79e00 100644 --- a/wp-admin/includes/update.php +++ b/wp-admin/includes/update.php @@ -87,6 +87,68 @@ function find_core_auto_update() { return $auto_update; } +/** + * Gets and caches the checksums for the given versions of WordPress + * + * @since 3.7.0 + * + * @param $version string|array A single version, or an array of versions to fetch + * + * @return bool|array False on failure, otherwise the array of checksums, keyed by version + */ +function get_core_checksums( $version ) { + if ( ! is_array( $version ) ) + $version = array( $version ); + + $return = array(); + + // Check to see if we have cached copies available, if we do, no need to request them + foreach ( $version as $i => $v ) { + if ( $checksums = get_site_transient( "core_checksums_$v" ) ) { + unset( $version[ $i ] ); + $return[ $v ] = $checksums; + } + } + + // We had cached copies for all of the versions! + if ( empty( $version ) ) + return $return; + + $url = 'http://api.wordpress.org/core/checksums/1.0/?' . http_build_query( array( 'version' => $version ), null, '&' ); + + if ( wp_http_supports( array( 'ssl' ) ) ) + $url = set_url_scheme( $url, 'https' ); + + $options = array( + 'timeout' => ( ( defined('DOING_CRON') && DOING_CRON ) ? 30 : 3 ), + ); + + $response = wp_remote_get( $url, $options ); + + if ( is_wp_error( $response ) || 200 != wp_remote_retrieve_response_code( $response ) ) + return false; + + $body = trim( wp_remote_retrieve_body( $response ) ); + $body = json_decode( $body, true ); + + if ( ! is_array( $body ) || ! isset( $body['checksums'] ) || ! is_array( $body['checksums'] ) ) + return false; + + // Cache the checksums for later + foreach ( $version as $v ) { + set_site_transient( "core_checksums_$v", $body['checksums'][ $v ], HOUR_IN_SECONDS ); + $return[ $v ] = $body['checksums'][ $v ]; + } + + // If the API didn't return anything for a version, explicitly set it's return value to false + foreach ( $return as $v => $r ) { + if ( empty( $r ) ) + $return[ $v ] = false; + } + + return $return; +} + function dismiss_core_update( $update ) { $dismissed = get_site_option( 'dismissed_update_core' ); $dismissed[ $update->current . '|' . $update->locale ] = true;