Make WordPress Core


Ignore:
Timestamp:
05/04/2023 02:34:58 AM (12 months 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.