--- a/wp/wp-admin/includes/class-wp-upgrader.php Thu Sep 29 08:06:27 2022 +0200
+++ b/wp/wp-admin/includes/class-wp-upgrader.php Fri Sep 05 18:40:08 2025 +0200
@@ -48,6 +48,7 @@
*
* @since 2.8.0
*/
+#[AllowDynamicProperties]
class WP_Upgrader {
/**
@@ -112,6 +113,26 @@
public $update_current = 0;
/**
+ * Stores the list of plugins or themes added to temporary backup directory.
+ *
+ * Used by the rollback functions.
+ *
+ * @since 6.3.0
+ * @var array
+ */
+ private $temp_backups = array();
+
+ /**
+ * Stores the list of plugins or themes to be restored from temporary backup directory.
+ *
+ * Used by the rollback functions.
+ *
+ * @since 6.3.0
+ * @var array
+ */
+ private $temp_restores = array();
+
+ /**
* Construct the upgrader with a skin.
*
* @since 2.8.0
@@ -128,34 +149,54 @@
}
/**
- * Initialize the upgrader.
+ * Initializes the upgrader.
*
* This will set the relationship between the skin being used and this upgrader,
* and also add the generic strings to `WP_Upgrader::$strings`.
*
+ * Additionally, it will schedule a weekly task to clean up the temporary backup directory.
+ *
* @since 2.8.0
+ * @since 6.3.0 Added the `schedule_temp_backup_cleanup()` task.
*/
public function init() {
$this->skin->set_upgrader( $this );
$this->generic_strings();
+
+ if ( ! wp_installing() ) {
+ $this->schedule_temp_backup_cleanup();
+ }
}
/**
- * Add the generic strings to WP_Upgrader::$strings.
+ * Schedules the cleanup of the temporary backup directory.
+ *
+ * @since 6.3.0
+ */
+ protected function schedule_temp_backup_cleanup() {
+ if ( false === wp_next_scheduled( 'wp_delete_temp_updater_backups' ) ) {
+ wp_schedule_event( time(), 'weekly', 'wp_delete_temp_updater_backups' );
+ }
+ }
+
+ /**
+ * Adds the generic strings to WP_Upgrader::$strings.
*
* @since 2.8.0
*/
public function generic_strings() {
- $this->strings['bad_request'] = __( 'Invalid data provided.' );
- $this->strings['fs_unavailable'] = __( 'Could not access filesystem.' );
- $this->strings['fs_error'] = __( 'Filesystem error.' );
- $this->strings['fs_no_root_dir'] = __( 'Unable to locate WordPress root directory.' );
- $this->strings['fs_no_content_dir'] = __( 'Unable to locate WordPress content directory (wp-content).' );
+ $this->strings['bad_request'] = __( 'Invalid data provided.' );
+ $this->strings['fs_unavailable'] = __( 'Could not access filesystem.' );
+ $this->strings['fs_error'] = __( 'Filesystem error.' );
+ $this->strings['fs_no_root_dir'] = __( 'Unable to locate WordPress root directory.' );
+ /* translators: %s: Directory name. */
+ $this->strings['fs_no_content_dir'] = sprintf( __( 'Unable to locate WordPress content directory (%s).' ), 'wp-content' );
$this->strings['fs_no_plugins_dir'] = __( 'Unable to locate WordPress plugin directory.' );
$this->strings['fs_no_themes_dir'] = __( 'Unable to locate WordPress theme directory.' );
/* translators: %s: Directory name. */
$this->strings['fs_no_folder'] = __( 'Unable to locate needed folder (%s).' );
+ $this->strings['no_package'] = __( 'Package not available.' );
$this->strings['download_failed'] = __( 'Download failed.' );
$this->strings['installing_package'] = __( 'Installing the latest version…' );
$this->strings['no_files'] = __( 'The package contains no files.' );
@@ -166,10 +207,19 @@
$this->strings['maintenance_start'] = __( 'Enabling Maintenance mode…' );
$this->strings['maintenance_end'] = __( 'Disabling Maintenance mode…' );
+
+ /* translators: %s: upgrade-temp-backup */
+ $this->strings['temp_backup_mkdir_failed'] = sprintf( __( 'Could not create the %s directory.' ), 'upgrade-temp-backup' );
+ /* translators: %s: upgrade-temp-backup */
+ $this->strings['temp_backup_move_failed'] = sprintf( __( 'Could not move the old version to the %s directory.' ), 'upgrade-temp-backup' );
+ /* translators: %s: The plugin or theme slug. */
+ $this->strings['temp_backup_restore_failed'] = __( 'Could not restore the original version of %s.' );
+ /* translators: %s: The plugin or theme slug. */
+ $this->strings['temp_backup_delete_failed'] = __( 'Could not delete the temporary backup directory for %s.' );
}
/**
- * Connect to the filesystem.
+ * Connects to the filesystem.
*
* @since 2.8.0
*
@@ -241,7 +291,7 @@
}
/**
- * Download a package.
+ * Downloads a package.
*
* @since 2.8.0
* @since 5.2.0 Added the `$check_signatures` parameter.
@@ -291,7 +341,7 @@
}
/**
- * Unpack a compressed package file.
+ * Unpacks a compressed package file.
*
* @since 2.8.0
*
@@ -307,6 +357,10 @@
$this->skin->feedback( 'unpack_package' );
+ if ( ! $wp_filesystem->wp_content_dir() ) {
+ return new WP_Error( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
+ }
+
$upgrade_folder = $wp_filesystem->wp_content_dir() . 'upgrade/';
// Clean up contents of upgrade directory beforehand.
@@ -345,7 +399,7 @@
}
/**
- * Flatten the results of WP_Filesystem_Base::dirlist() for iterating over.
+ * Flattens the results of WP_Filesystem_Base::dirlist() for iterating over.
*
* @since 4.9.0
* @access protected
@@ -428,6 +482,7 @@
* clear out the destination folder if it already exists.
*
* @since 2.8.0
+ * @since 6.2.0 Use move_dir() instead of copy_dir() when possible.
*
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
* @global array $wp_theme_directories
@@ -469,9 +524,14 @@
$destination = $args['destination'];
$clear_destination = $args['clear_destination'];
- set_time_limit( 300 );
+ if ( function_exists( 'set_time_limit' ) ) {
+ set_time_limit( 300 );
+ }
- if ( empty( $source ) || empty( $destination ) ) {
+ if (
+ ( ! is_string( $source ) || '' === $source || trim( $source ) !== $source ) ||
+ ( ! is_string( $destination ) || '' === $destination || trim( $destination ) !== $destination )
+ ) {
return new WP_Error( 'bad_request', $this->strings['bad_request'] );
}
$this->skin->feedback( 'installing_package' );
@@ -508,8 +568,10 @@
// There are no files?
return new WP_Error( 'incompatible_archive_empty', $this->strings['incompatible_archive'], $this->strings['no_files'] );
} else {
- // It's only a single file, the upgrader will use the folder name of this file as the destination folder.
- // Folder name is based on zip filename.
+ /*
+ * It's only a single file, the upgrader will use the folder name of this file as the destination folder.
+ * Folder name is based on zip filename.
+ */
$source = trailingslashit( $args['source'] );
}
@@ -530,6 +592,16 @@
return $source;
}
+ if ( ! empty( $args['hook_extra']['temp_backup'] ) ) {
+ $temp_backup = $this->move_to_temp_backup_dir( $args['hook_extra']['temp_backup'] );
+
+ if ( is_wp_error( $temp_backup ) ) {
+ return $temp_backup;
+ }
+
+ $this->temp_backups[] = $args['hook_extra']['temp_backup'];
+ }
+
// Has the source location changed? If so, we need a new source_files list.
if ( $source !== $remote_source ) {
$source_files = array_keys( $wp_filesystem->dirlist( $source ) );
@@ -576,8 +648,10 @@
return $removed;
}
} elseif ( $args['abort_if_destination_exists'] && $wp_filesystem->exists( $remote_destination ) ) {
- // If we're not clearing the destination folder and something exists there already, bail.
- // But first check to see if there are actually any files in the folder.
+ /*
+ * If we're not clearing the destination folder and something exists there already, bail.
+ * But first check to see if there are actually any files in the folder.
+ */
$_files = $wp_filesystem->dirlist( $remote_destination );
if ( ! empty( $_files ) ) {
$wp_filesystem->delete( $remote_source, true ); // Clear out the source files.
@@ -585,25 +659,38 @@
}
}
- // Create destination if needed.
- if ( ! $wp_filesystem->exists( $remote_destination ) ) {
- if ( ! $wp_filesystem->mkdir( $remote_destination, FS_CHMOD_DIR ) ) {
- return new WP_Error( 'mkdir_failed_destination', $this->strings['mkdir_failed'], $remote_destination );
+ /*
+ * If 'clear_working' is false, the source should not be removed, so use copy_dir() instead.
+ *
+ * Partial updates, like language packs, may want to retain the destination.
+ * If the destination exists or has contents, this may be a partial update,
+ * and the destination should not be removed, so use copy_dir() instead.
+ */
+ if ( $args['clear_working']
+ && (
+ // Destination does not exist or has no contents.
+ ! $wp_filesystem->exists( $remote_destination )
+ || empty( $wp_filesystem->dirlist( $remote_destination ) )
+ )
+ ) {
+ $result = move_dir( $source, $remote_destination, true );
+ } else {
+ // Create destination if needed.
+ if ( ! $wp_filesystem->exists( $remote_destination ) ) {
+ if ( ! $wp_filesystem->mkdir( $remote_destination, FS_CHMOD_DIR ) ) {
+ return new WP_Error( 'mkdir_failed_destination', $this->strings['mkdir_failed'], $remote_destination );
+ }
}
+ $result = copy_dir( $source, $remote_destination );
}
- // Copy new version of item into place.
- $result = copy_dir( $source, $remote_destination );
- if ( is_wp_error( $result ) ) {
- if ( $args['clear_working'] ) {
- $wp_filesystem->delete( $remote_source, true );
- }
- return $result;
+ // Clear the working directory?
+ if ( $args['clear_working'] ) {
+ $wp_filesystem->delete( $remote_source, true );
}
- // Clear the working folder?
- if ( $args['clear_working'] ) {
- $wp_filesystem->delete( $remote_source, true );
+ if ( is_wp_error( $result ) ) {
+ return $result;
}
$destination_name = basename( str_replace( $local_destination, '', $destination ) );
@@ -634,7 +721,7 @@
}
/**
- * Run an upgrade/installation.
+ * Runs an upgrade/installation.
*
* Attempts to download the package (if it is not a local file), unpack it, and
* install it in the destination folder.
@@ -741,10 +828,12 @@
* Download the package. Note: If the package is the full path
* to an existing local file, it will be returned untouched.
*/
- $download = $this->download_package( $options['package'], true, $options['hook_extra'] );
+ $download = $this->download_package( $options['package'], false, $options['hook_extra'] );
- // Allow for signature soft-fail.
- // WARNING: This may be removed in the future.
+ /*
+ * Allow for signature soft-fail.
+ * WARNING: This may be removed in the future.
+ */
if ( is_wp_error( $download ) && $download->get_error_data( 'softfail-filename' ) ) {
// Don't output the 'no signature could be found' failure message for now.
@@ -810,7 +899,24 @@
$result = apply_filters( 'upgrader_install_package_result', $result, $options['hook_extra'] );
$this->skin->set_result( $result );
+
if ( is_wp_error( $result ) ) {
+ // An automatic plugin update will have already performed its rollback.
+ if ( ! empty( $options['hook_extra']['temp_backup'] ) ) {
+ $this->temp_restores[] = $options['hook_extra']['temp_backup'];
+
+ /*
+ * Restore the backup on shutdown.
+ * Actions running on `shutdown` are immune to PHP timeouts,
+ * so in case the failure was due to a PHP timeout,
+ * it will still be able to properly restore the previous version.
+ *
+ * Zero arguments are accepted as a string can sometimes be passed
+ * internally during actions, causing an error because
+ * `WP_Upgrader::restore_temp_backup()` expects an array.
+ */
+ add_action( 'shutdown', array( $this, 'restore_temp_backup' ), 10, 0 );
+ }
$this->skin->error( $result );
if ( ! method_exists( $this->skin, 'hide_process_failed' ) || ! $this->skin->hide_process_failed( $result ) ) {
@@ -823,6 +929,12 @@
$this->skin->after();
+ // Clean up the backup kept in the temporary backup directory.
+ if ( ! empty( $options['hook_extra']['temp_backup'] ) ) {
+ // Delete the backup on `shutdown` to avoid a PHP timeout.
+ add_action( 'shutdown', array( $this, 'delete_temp_backup' ), 100, 0 );
+ }
+
if ( ! $options['is_multi'] ) {
/**
@@ -864,7 +976,7 @@
}
/**
- * Toggle maintenance mode for the site.
+ * Toggles maintenance mode for the site.
*
* Creates/deletes the maintenance file to enable/disable maintenance mode.
*
@@ -876,15 +988,25 @@
*/
public function maintenance_mode( $enable = false ) {
global $wp_filesystem;
+
+ if ( ! $wp_filesystem ) {
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ WP_Filesystem();
+ }
+
$file = $wp_filesystem->abspath() . '.maintenance';
if ( $enable ) {
- $this->skin->feedback( 'maintenance_start' );
+ if ( ! wp_doing_cron() ) {
+ $this->skin->feedback( 'maintenance_start' );
+ }
// Create maintenance file to signal that we are upgrading.
$maintenance_string = '<?php $upgrading = ' . time() . '; ?>';
$wp_filesystem->delete( $file );
$wp_filesystem->put_contents( $file, $maintenance_string, FS_CHMOD_FILE );
} elseif ( ! $enable && $wp_filesystem->exists( $file ) ) {
- $this->skin->feedback( 'maintenance_end' );
+ if ( ! wp_doing_cron() ) {
+ $this->skin->feedback( 'maintenance_end' );
+ }
$wp_filesystem->delete( $file );
}
}
@@ -894,6 +1016,8 @@
*
* @since 4.5.0
*
+ * @global wpdb $wpdb The WordPress database abstraction object.
+ *
* @param string $lock_name The name of this unique lock.
* @param int $release_timeout Optional. The duration in seconds to respect an existing lock.
* Default: 1 hour.
@@ -907,7 +1031,7 @@
$lock_option = $lock_name . '.lock';
// Try to lock.
- $lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", $lock_option, time() ) );
+ $lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'off') /* LOCK */", $lock_option, time() ) );
if ( ! $lock_result ) {
$lock_result = get_option( $lock_option );
@@ -947,6 +1071,201 @@
public static function release_lock( $lock_name ) {
return delete_option( $lock_name . '.lock' );
}
+
+ /**
+ * Moves the plugin or theme being updated into a temporary backup directory.
+ *
+ * @since 6.3.0
+ *
+ * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
+ *
+ * @param string[] $args {
+ * Array of data for the temporary backup.
+ *
+ * @type string $slug Plugin or theme slug.
+ * @type string $src Path to the root directory for plugins or themes.
+ * @type string $dir Destination subdirectory name. Accepts 'plugins' or 'themes'.
+ * }
+ *
+ * @return bool|WP_Error True on success, false on early exit, otherwise WP_Error.
+ */
+ public function move_to_temp_backup_dir( $args ) {
+ global $wp_filesystem;
+
+ if ( empty( $args['slug'] ) || empty( $args['src'] ) || empty( $args['dir'] ) ) {
+ return false;
+ }
+
+ /*
+ * Skip any plugin that has "." as its slug.
+ * A slug of "." will result in a `$src` value ending in a period.
+ *
+ * On Windows, this will cause the 'plugins' folder to be moved,
+ * and will cause a failure when attempting to call `mkdir()`.
+ */
+ if ( '.' === $args['slug'] ) {
+ return false;
+ }
+
+ if ( ! $wp_filesystem->wp_content_dir() ) {
+ return new WP_Error( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
+ }
+
+ $dest_dir = $wp_filesystem->wp_content_dir() . 'upgrade-temp-backup/';
+ $sub_dir = $dest_dir . $args['dir'] . '/';
+
+ // Create the temporary backup directory if it does not exist.
+ if ( ! $wp_filesystem->is_dir( $sub_dir ) ) {
+ if ( ! $wp_filesystem->is_dir( $dest_dir ) ) {
+ $wp_filesystem->mkdir( $dest_dir, FS_CHMOD_DIR );
+ }
+
+ if ( ! $wp_filesystem->mkdir( $sub_dir, FS_CHMOD_DIR ) ) {
+ // Could not create the backup directory.
+ return new WP_Error( 'fs_temp_backup_mkdir', $this->strings['temp_backup_mkdir_failed'] );
+ }
+ }
+
+ $src_dir = $wp_filesystem->find_folder( $args['src'] );
+ $src = trailingslashit( $src_dir ) . $args['slug'];
+ $dest = $dest_dir . trailingslashit( $args['dir'] ) . $args['slug'];
+
+ // Delete the temporary backup directory if it already exists.
+ if ( $wp_filesystem->is_dir( $dest ) ) {
+ $wp_filesystem->delete( $dest, true );
+ }
+
+ // Move to the temporary backup directory.
+ $result = move_dir( $src, $dest, true );
+ if ( is_wp_error( $result ) ) {
+ return new WP_Error( 'fs_temp_backup_move', $this->strings['temp_backup_move_failed'] );
+ }
+
+ return true;
+ }
+
+ /**
+ * Restores the plugin or theme from temporary backup.
+ *
+ * @since 6.3.0
+ * @since 6.6.0 Added the `$temp_backups` parameter.
+ *
+ * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
+ *
+ * @param array[] $temp_backups {
+ * Optional. An array of temporary backups.
+ *
+ * @type array ...$0 {
+ * Information about the backup.
+ *
+ * @type string $dir The temporary backup location in the upgrade-temp-backup directory.
+ * @type string $slug The item's slug.
+ * @type string $src The directory where the original is stored. For example, `WP_PLUGIN_DIR`.
+ * }
+ * }
+ * @return bool|WP_Error True on success, false on early exit, otherwise WP_Error.
+ */
+ public function restore_temp_backup( array $temp_backups = array() ) {
+ global $wp_filesystem;
+
+ $errors = new WP_Error();
+
+ if ( empty( $temp_backups ) ) {
+ $temp_backups = $this->temp_restores;
+ }
+
+ foreach ( $temp_backups as $args ) {
+ if ( empty( $args['slug'] ) || empty( $args['src'] ) || empty( $args['dir'] ) ) {
+ return false;
+ }
+
+ if ( ! $wp_filesystem->wp_content_dir() ) {
+ $errors->add( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
+ return $errors;
+ }
+
+ $src = $wp_filesystem->wp_content_dir() . 'upgrade-temp-backup/' . $args['dir'] . '/' . $args['slug'];
+ $dest_dir = $wp_filesystem->find_folder( $args['src'] );
+ $dest = trailingslashit( $dest_dir ) . $args['slug'];
+
+ if ( $wp_filesystem->is_dir( $src ) ) {
+ // Cleanup.
+ if ( $wp_filesystem->is_dir( $dest ) && ! $wp_filesystem->delete( $dest, true ) ) {
+ $errors->add(
+ 'fs_temp_backup_delete',
+ sprintf( $this->strings['temp_backup_restore_failed'], $args['slug'] )
+ );
+ continue;
+ }
+
+ // Move it.
+ $result = move_dir( $src, $dest, true );
+ if ( is_wp_error( $result ) ) {
+ $errors->add(
+ 'fs_temp_backup_delete',
+ sprintf( $this->strings['temp_backup_restore_failed'], $args['slug'] )
+ );
+ continue;
+ }
+ }
+ }
+
+ return $errors->has_errors() ? $errors : true;
+ }
+
+ /**
+ * Deletes a temporary backup.
+ *
+ * @since 6.3.0
+ * @since 6.6.0 Added the `$temp_backups` parameter.
+ *
+ * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
+ *
+ * @param array[] $temp_backups {
+ * Optional. An array of temporary backups.
+ *
+ * @type array ...$0 {
+ * Information about the backup.
+ *
+ * @type string $dir The temporary backup location in the upgrade-temp-backup directory.
+ * @type string $slug The item's slug.
+ * @type string $src The directory where the original is stored. For example, `WP_PLUGIN_DIR`.
+ * }
+ * }
+ * @return bool|WP_Error True on success, false on early exit, otherwise WP_Error.
+ */
+ public function delete_temp_backup( array $temp_backups = array() ) {
+ global $wp_filesystem;
+
+ $errors = new WP_Error();
+
+ if ( empty( $temp_backups ) ) {
+ $temp_backups = $this->temp_backups;
+ }
+
+ foreach ( $temp_backups as $args ) {
+ if ( empty( $args['slug'] ) || empty( $args['dir'] ) ) {
+ return false;
+ }
+
+ if ( ! $wp_filesystem->wp_content_dir() ) {
+ $errors->add( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
+ return $errors;
+ }
+
+ $temp_backup_dir = $wp_filesystem->wp_content_dir() . "upgrade-temp-backup/{$args['dir']}/{$args['slug']}";
+
+ if ( ! $wp_filesystem->delete( $temp_backup_dir, true ) ) {
+ $errors->add(
+ 'temp_backup_delete_failed',
+ sprintf( $this->strings['temp_backup_delete_failed'], $args['slug'] )
+ );
+ continue;
+ }
+ }
+
+ return $errors->has_errors() ? $errors : true;
+ }
}
/** Plugin_Upgrader class */