Make WordPress Core


Ignore:
Timestamp:
05/04/2023 02:34:58 AM (3 years ago)
Author:
SergeyBiryukov
Message:

Upgrade/Install: Create a temporary backup of plugins and themes before updating.

This aims to make the update process more reliable and ensures that if a plugin or theme update fails, the previous version can be safely restored.

  • When updating a plugin or theme, the old version is moved to a temporary backup directory:
    • wp-content/upgrade-temp-backup/plugins/[plugin-slug] for plugins
    • wp-content/upgrade-temp-backup/themes/[theme-slug] for themes.
  • If the update fails, then the backup kept in the temporary backup directory is restored to its original location.
  • If the update succeeds, the temporary backup is deleted.

To further help troubleshoot plugin and theme updates, two new checks were added to the Site Health screen:

  • A check to make sure that the upgrade-temp-backup directory is writable.
  • A check that there is enough disk space available to safely perform updates.

To avoid confusion: The temporary backup directory will NOT be used to “roll back” a plugin to a previous version after a completed update. This directory will simply contain a transient backup of the previous version of a plugin or theme being updated, and as soon as the update process finishes, the directory will be empty.

Follow-up to [55204], [55220].

Props afragen, costdev, pbiron, azaozz, hellofromTonya, aristath, peterwilsoncc, TJNowell, bronsonquick, Clorith, dd32, poena, TimothyBlynJacobs, audrasjb, mikeschroder, a2hosting, KZeni, galbaras, richards1052, Boniu91, mai21, francina, TobiasBg, desrosj, noisysocks, johnbillion, dlh, chaion07, davidbaumwald, jrf, thisisyeasin, ignatggeorgiev, SergeyBiryukov.
Fixes #51857.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/class-wp-upgrader.php

    r55258 r55720  
    114114
    115115        /**
     116         * Stores the list of plugins or themes added to temporary backup directory.
     117         *
     118         * Used by the rollback functions.
     119         *
     120         * @since 6.3.0
     121         * @var array
     122         */
     123        private $temp_backups = array();
     124
     125        /**
     126         * Stores the list of plugins or themes to be restored from temporary backup directory.
     127         *
     128         * Used by the rollback functions.
     129         *
     130         * @since 6.3.0
     131         * @var array
     132         */
     133        private $temp_restores = array();
     134
     135        /**
    116136         * Construct the upgrader with a skin.
    117137         *
     
    135155         * and also add the generic strings to `WP_Upgrader::$strings`.
    136156         *
     157         * Additionally, it will schedule a weekly task to clean up the temporary backup directory.
     158         *
    137159         * @since 2.8.0
     160         * @since 6.3.0 Added the `schedule_temp_backup_cleanup()` task.
    138161         */
    139162        public function init() {
    140163                $this->skin->set_upgrader( $this );
    141164                $this->generic_strings();
     165
     166                if ( ! wp_installing() ) {
     167                        $this->schedule_temp_backup_cleanup();
     168                }
     169        }
     170
     171        /**
     172         * Schedules the cleanup of the temporary backup directory.
     173         *
     174         * @since 6.3.0
     175         */
     176        protected function schedule_temp_backup_cleanup() {
     177                if ( false === wp_next_scheduled( 'wp_delete_temp_updater_backups' ) ) {
     178                        wp_schedule_event( time(), 'weekly', 'wp_delete_temp_updater_backups' );
     179                }
    142180        }
    143181
     
    168206                $this->strings['maintenance_start'] = __( 'Enabling Maintenance mode…' );
    169207                $this->strings['maintenance_end']   = __( 'Disabling Maintenance mode…' );
     208
     209                /* translators: %s: upgrade-temp-backup */
     210                $this->strings['temp_backup_mkdir_failed'] = sprintf( __( 'Could not create the %s directory.' ), 'upgrade-temp-backup' );
     211                /* translators: %s: upgrade-temp-backup */
     212                $this->strings['temp_backup_move_failed'] = sprintf( __( 'Could not move the old version to the %s directory.' ), 'upgrade-temp-backup' );
     213                /* translators: %s: The plugin or theme slug. */
     214                $this->strings['temp_backup_restore_failed'] = __( 'Could not restore the original version of %s.' );
     215                /* translators: %s: The plugin or theme slug. */
     216                $this->strings['temp_backup_delete_failed'] = __( 'Could not delete the temporary backup directory for %s.' );
    170217        }
    171218
     
    309356                $this->skin->feedback( 'unpack_package' );
    310357
     358                if ( ! $wp_filesystem->wp_content_dir() ) {
     359                        return new WP_Error( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
     360                }
     361
    311362                $upgrade_folder = $wp_filesystem->wp_content_dir() . 'upgrade/';
    312363
     
    533584                if ( is_wp_error( $source ) ) {
    534585                        return $source;
     586                }
     587
     588                if ( ! empty( $args['hook_extra']['temp_backup'] ) ) {
     589                        $temp_backup = $this->move_to_temp_backup_dir( $args['hook_extra']['temp_backup'] );
     590
     591                        if ( is_wp_error( $temp_backup ) ) {
     592                                return $temp_backup;
     593                        }
     594
     595                        $this->temp_backups[] = $args['hook_extra']['temp_backup'];
    535596                }
    536597
     
    615676                }
    616677
    617                 // Clear the working folder?
     678                // Clear the working directory?
    618679                if ( $args['clear_working'] ) {
    619680                        $wp_filesystem->delete( $remote_source, true );
     
    828889
    829890                $this->skin->set_result( $result );
     891
    830892                if ( is_wp_error( $result ) ) {
     893                        if ( ! empty( $options['hook_extra']['temp_backup'] ) ) {
     894                                $this->temp_restores[] = $options['hook_extra']['temp_backup'];
     895
     896                                /*
     897                                 * Restore the backup on shutdown.
     898                                 * Actions running on `shutdown` are immune to PHP timeouts,
     899                                 * so in case the failure was due to a PHP timeout,
     900                                 * it will still be able to properly restore the previous version.
     901                                 */
     902                                add_action( 'shutdown', array( $this, 'restore_temp_backup' ) );
     903                        }
    831904                        $this->skin->error( $result );
    832905
     
    840913
    841914                $this->skin->after();
     915
     916                // Clean up the backup kept in the temporary backup directory.
     917                if ( ! empty( $options['hook_extra']['temp_backup'] ) ) {
     918                        // Delete the backup on `shutdown` to avoid a PHP timeout.
     919                        add_action( 'shutdown', array( $this, 'delete_temp_backup' ), 100, 0 );
     920                }
    842921
    843922                if ( ! $options['is_multi'] ) {
     
    9671046                return delete_option( $lock_name . '.lock' );
    9681047        }
     1048
     1049        /**
     1050         * Moves the plugin or theme being updated into a temporary backup directory.
     1051         *
     1052         * @since 6.3.0
     1053         *
     1054         * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
     1055         *
     1056         * @param string[] $args {
     1057         *     Array of data for the temporary backup.
     1058         *
     1059         *     @type string $slug Plugin or theme slug.
     1060         *     @type string $src  Path to the root directory for plugins or themes.
     1061         *     @type string $dir  Destination subdirectory name. Accepts 'plugins' or 'themes'.
     1062         * }
     1063         *
     1064         * @return bool|WP_Error True on success, false on early exit, otherwise WP_Error.
     1065         */
     1066        public function move_to_temp_backup_dir( $args ) {
     1067                global $wp_filesystem;
     1068
     1069                if ( empty( $args['slug'] ) || empty( $args['src'] ) || empty( $args['dir'] ) ) {
     1070                        return false;
     1071                }
     1072
     1073                /*
     1074                 * Skip any plugin that has "." as its slug.
     1075                 * A slug of "." will result in a `$src` value ending in a period.
     1076                 *
     1077                 * On Windows, this will cause the 'plugins' folder to be moved,
     1078                 * and will cause a failure when attempting to call `mkdir()`.
     1079                 */
     1080                if ( '.' === $args['slug'] ) {
     1081                        return false;
     1082                }
     1083
     1084                if ( ! $wp_filesystem->wp_content_dir() ) {
     1085                        return new WP_Error( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
     1086                }
     1087
     1088                $dest_dir = $wp_filesystem->wp_content_dir() . 'upgrade-temp-backup/';
     1089                $sub_dir  = $dest_dir . $args['dir'] . '/';
     1090
     1091                // Create the temporary backup directory if it does not exist.
     1092                if ( ! $wp_filesystem->is_dir( $sub_dir ) ) {
     1093                        if ( ! $wp_filesystem->is_dir( $dest_dir ) ) {
     1094                                $wp_filesystem->mkdir( $dest_dir, FS_CHMOD_DIR );
     1095                        }
     1096
     1097                        if ( ! $wp_filesystem->mkdir( $sub_dir, FS_CHMOD_DIR ) ) {
     1098                                // Could not create the backup directory.
     1099                                return new WP_Error( 'fs_temp_backup_mkdir', $this->strings['temp_backup_mkdir_failed'] );
     1100                        }
     1101                }
     1102
     1103                $src_dir = $wp_filesystem->find_folder( $args['src'] );
     1104                $src     = trailingslashit( $src_dir ) . $args['slug'];
     1105                $dest    = $dest_dir . trailingslashit( $args['dir'] ) . $args['slug'];
     1106
     1107                // Delete the temporary backup directory if it already exists.
     1108                if ( $wp_filesystem->is_dir( $dest ) ) {
     1109                        $wp_filesystem->delete( $dest, true );
     1110                }
     1111
     1112                // Move to the temporary backup directory.
     1113                $result = move_dir( $src, $dest, true );
     1114                if ( is_wp_error( $result ) ) {
     1115                        return new WP_Error( 'fs_temp_backup_move', $this->strings['temp_backup_move_failed'] );
     1116                }
     1117
     1118                return true;
     1119        }
     1120
     1121        /**
     1122         * Restores the plugin or theme from temporary backup.
     1123         *
     1124         * @since 6.3.0
     1125         *
     1126         * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
     1127         *
     1128         * @return bool|WP_Error True on success, false on early exit, otherwise WP_Error.
     1129         */
     1130        public function restore_temp_backup() {
     1131                global $wp_filesystem;
     1132
     1133                $errors = new WP_Error();
     1134
     1135                foreach ( $this->temp_restores as $args ) {
     1136                        if ( empty( $args['slug'] ) || empty( $args['src'] ) || empty( $args['dir'] ) ) {
     1137                                return false;
     1138                        }
     1139
     1140                        if ( ! $wp_filesystem->wp_content_dir() ) {
     1141                                $errors->add( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
     1142                                return $errors;
     1143                        }
     1144
     1145                        $src      = $wp_filesystem->wp_content_dir() . 'upgrade-temp-backup/' . $args['dir'] . '/' . $args['slug'];
     1146                        $dest_dir = $wp_filesystem->find_folder( $args['src'] );
     1147                        $dest     = trailingslashit( $dest_dir ) . $args['slug'];
     1148
     1149                        if ( $wp_filesystem->is_dir( $src ) ) {
     1150                                // Cleanup.
     1151                                if ( $wp_filesystem->is_dir( $dest ) && ! $wp_filesystem->delete( $dest, true ) ) {
     1152                                        $errors->add(
     1153                                                'fs_temp_backup_delete',
     1154                                                sprintf( $this->strings['temp_backup_restore_failed'], $args['slug'] )
     1155                                        );
     1156                                        continue;
     1157                                }
     1158
     1159                                // Move it.
     1160                                $result = move_dir( $src, $dest, true );
     1161                                if ( is_wp_error( $result ) ) {
     1162                                        $errors->add(
     1163                                                'fs_temp_backup_delete',
     1164                                                sprintf( $this->strings['temp_backup_restore_failed'], $args['slug'] )
     1165                                        );
     1166                                        continue;
     1167                                }
     1168                        }
     1169                }
     1170
     1171                return $errors->has_errors() ? $errors : true;
     1172        }
     1173
     1174        /**
     1175         * Deletes a temporary backup.
     1176         *
     1177         * @since 6.3.0
     1178         *
     1179         * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
     1180         *
     1181         * @return bool|WP_Error True on success, false on early exit, otherwise WP_Error.
     1182         */
     1183        public function delete_temp_backup() {
     1184                global $wp_filesystem;
     1185
     1186                $errors = new WP_Error();
     1187
     1188                foreach ( $this->temp_backups as $args ) {
     1189                        if ( empty( $args['slug'] ) || empty( $args['dir'] ) ) {
     1190                                return false;
     1191                        }
     1192
     1193                        if ( ! $wp_filesystem->wp_content_dir() ) {
     1194                                $errors->add( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
     1195                                return $errors;
     1196                        }
     1197
     1198                        $temp_backup_dir = $wp_filesystem->wp_content_dir() . "upgrade-temp-backup/{$args['dir']}/{$args['slug']}";
     1199
     1200                        if ( ! $wp_filesystem->delete( $temp_backup_dir, true ) ) {
     1201                                $errors->add(
     1202                                        'temp_backup_delete_failed',
     1203                                        sprintf( $this->strings['temp_backup_delete_failed'] ),
     1204                                        $args['slug']
     1205                                );
     1206                                continue;
     1207                        }
     1208                }
     1209
     1210                return $errors->has_errors() ? $errors : true;
     1211        }
    9691212}
    9701213
Note: See TracChangeset for help on using the changeset viewer.