diff --git a/src/wp-admin/includes/class-plugin-upgrader.php b/src/wp-admin/includes/class-plugin-upgrader.php index 76083b5211..aeefaf9875 100644 --- a/src/wp-admin/includes/class-plugin-upgrader.php +++ b/src/wp-admin/includes/class-plugin-upgrader.php @@ -226,9 +226,14 @@ class Plugin_Upgrader extends WP_Upgrader { 'clear_destination' => true, 'clear_working' => true, 'hook_extra' => array( - 'plugin' => $plugin, - 'type' => 'plugin', - 'action' => 'update', + 'plugin' => $plugin, + 'type' => 'plugin', + 'action' => 'update', + 'temp_backup' => array( + 'slug' => dirname( $plugin ), + 'src' => WP_PLUGIN_DIR, + 'dir' => 'plugins', + ), ), ) ); @@ -342,7 +347,12 @@ class Plugin_Upgrader extends WP_Upgrader { 'clear_working' => true, 'is_multi' => true, 'hook_extra' => array( - 'plugin' => $plugin, + 'plugin' => $plugin, + 'temp_backup' => array( + 'slug' => dirname( $plugin ), + 'src' => WP_PLUGIN_DIR, + 'dir' => 'plugins', + ), ), ) ); diff --git a/src/wp-admin/includes/class-theme-upgrader.php b/src/wp-admin/includes/class-theme-upgrader.php index c550314c5d..3a7beaf1fd 100644 --- a/src/wp-admin/includes/class-theme-upgrader.php +++ b/src/wp-admin/includes/class-theme-upgrader.php @@ -328,9 +328,14 @@ class Theme_Upgrader extends WP_Upgrader { 'clear_destination' => true, 'clear_working' => true, 'hook_extra' => array( - 'theme' => $theme, - 'type' => 'theme', - 'action' => 'update', + 'theme' => $theme, + 'type' => 'theme', + 'action' => 'update', + 'temp_backup' => array( + 'slug' => $theme, + 'src' => get_theme_root( $theme ), + 'dir' => 'themes', + ), ), ) ); @@ -443,7 +448,12 @@ class Theme_Upgrader extends WP_Upgrader { 'clear_working' => true, 'is_multi' => true, 'hook_extra' => array( - 'theme' => $theme, + 'theme' => $theme, + 'temp_backup' => array( + 'slug' => $theme, + 'src' => get_theme_root( $theme ), + 'dir' => 'themes', + ), ), ) ); diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index 884b37ca5a..8a6fa79827 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -1882,6 +1882,196 @@ class WP_Site_Health { return $result; } + /** + * Test available disk space for updates. + * + * @since 5.9.0 + * + * @return array The test results. + */ + public function get_test_available_updates_disk_space() { + $available_space = function_exists( 'disk_free_space' ) ? (int) @disk_free_space( WP_CONTENT_DIR . '/upgrade/' ) : false; + $available_space_in_mb = $available_space / MB_IN_BYTES; + + $result = array( + 'label' => __( 'Disk space available to safely perform updates' ), + 'status' => 'good', + 'badge' => array( + 'label' => __( 'Security' ), + 'color' => 'blue', + ), + 'description' => sprintf( + /* translators: %s: Available disk space in MB or GB. */ + '
' . __( '%s available disk space was detected, update routines can be performed safely.' ), + size_format( $available_space ) + ), + 'actions' => '', + 'test' => 'available_updates_disk_space', + ); + + if ( $available_space_in_mb < 100 ) { + $result['description'] = __( 'Available disk space is low, less than 100 MB available.' ); + $result['status'] = 'recommended'; + } + + if ( $available_space_in_mb < 20 ) { + $result['description'] = __( 'Available disk space is critically low, less than 20 MB available. Proceed with caution, updates may fail.' ); + $result['status'] = 'critical'; + } + + if ( ! $available_space ) { + $result['description'] = __( 'Could not determine available disk space for updates.' ); + $result['status'] = 'recommended'; + } + + return $result; + } + + /** + * Test if plugin and theme updates temp-backup directories are writable or can be created. + * + * @since 5.9.0 + * + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + * + * @return array The test results. + */ + public function get_test_update_temp_backup_writable() { + global $wp_filesystem; + + $result = array( + 'label' => sprintf( + /* translators: %s: temp-backup */ + __( 'Plugin and theme update %s directory is writable' ), + 'temp-backup' + ), + 'status' => 'good', + 'badge' => array( + 'label' => __( 'Security' ), + 'color' => 'blue', + ), + 'description' => sprintf( + /* translators: %s: wp-content/upgrade/temp-backup */ + '
' . __( 'The %s directory used to improve the stability of plugin and theme updates is writable.' ),
+ 'wp-content/upgrade/temp-backup'
+ ),
+ 'actions' => '',
+ 'test' => 'update_temp_backup_writable',
+ );
+
+ if ( ! $wp_filesystem ) {
+ if ( ! function_exists( 'WP_Filesystem' ) ) {
+ require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' );
+ }
+ WP_Filesystem();
+ }
+ $wp_content = $wp_filesystem->wp_content_dir();
+
+ $upgrade_dir_exists = $wp_filesystem->is_dir( "$wp_content/upgrade" );
+ $upgrade_dir_is_writable = $wp_filesystem->is_writable( "$wp_content/upgrade" );
+ $backup_dir_exists = $wp_filesystem->is_dir( "$wp_content/upgrade/temp-backup" );
+ $backup_dir_is_writable = $wp_filesystem->is_writable( "$wp_content/upgrade/temp-backup" );
+
+ $plugins_dir_exists = $wp_filesystem->is_dir( "$wp_content/upgrade/temp-backup/plugins" );
+ $plugins_dir_is_writable = $wp_filesystem->is_writable( "$wp_content/upgrade/temp-backup/plugins" );
+ $themes_dir_exists = $wp_filesystem->is_dir( "$wp_content/upgrade/temp-backup/themes" );
+ $themes_dir_is_writable = $wp_filesystem->is_writable( "$wp_content/upgrade/temp-backup/themes" );
+
+ if ( $plugins_dir_exists && ! $plugins_dir_is_writable && $themes_dir_exists && ! $themes_dir_is_writable ) {
+ $result['status'] = 'critical';
+ $result['label'] = sprintf(
+ /* translators: %s: temp-backup */
+ __( 'Plugins and themes %s directories exist but are not writable' ),
+ 'temp-backup'
+ );
+ $result['description'] = sprintf(
+ /* translators: 1: wp-content/upgrade/temp-backup/plugins, 2: wp-content/upgrade/temp-backup/themes. */
+ '
' . __( 'The %1$s and %2$s directories exist but are not writable. These directories are used to improve the stability of plugin updates. Please make sure the server has write permissions to these directories.' ) . '
', + 'wp-content/upgrade/temp-backup/plugins',
+ 'wp-content/upgrade/temp-backup/themes'
+ );
+ return $result;
+ }
+
+ if ( $plugins_dir_exists && ! $plugins_dir_is_writable ) {
+ $result['status'] = 'critical';
+ $result['label'] = sprintf(
+ /* translators: %s: temp-backup */
+ __( 'Plugins %s directory exists but is not writable' ),
+ 'temp-backup'
+ );
+ $result['description'] = sprintf(
+ /* translators: %s: wp-content/upgrade/temp-backup/plugins */
+ '' . __( 'The %s directory exists but is not writable. This directory is used to improve the stability of plugin updates. Please make sure the server has write permissions to this directory.' ) . '
', + 'wp-content/upgrade/temp-backup/plugins'
+ );
+ return $result;
+ }
+
+ if ( $themes_dir_exists && ! $themes_dir_is_writable ) {
+ $result['status'] = 'critical';
+ $result['label'] = sprintf(
+ /* translators: %s: temp-backup */
+ __( 'Themes %s directory exists but is not writable' ),
+ 'temp-backup'
+ );
+ $result['description'] = sprintf(
+ /* translators: %s: wp-content/upgrade/temp-backup/themes */
+ '' . __( 'The %s directory exists but is not writable. This directory is used to improve the stability of theme updates. Please make sure the server has write permissions to this directory.' ) . '
', + 'wp-content/upgrade/temp-backup/themes'
+ );
+ return $result;
+ }
+
+ if ( ( ! $plugins_dir_exists || ! $themes_dir_exists ) && $backup_dir_exists && ! $backup_dir_is_writable ) {
+ $result['status'] = 'critical';
+ $result['label'] = sprintf(
+ /* translators: %s: temp-backup */
+ __( 'The %s directory exists but is not writable' ),
+ 'temp-backup'
+ );
+ $result['description'] = sprintf(
+ /* translators: %s: wp-content/upgrade/temp-backup */
+ '' . __( 'The %s directory exists but is not writable. This directory is used to improve the stability of plugin and theme updates. Please make sure the server has write permissions to this directory.' ) . '
', + 'wp-content/upgrade/temp-backup'
+ );
+ return $result;
+ }
+
+ if ( ! $backup_dir_exists && $upgrade_dir_exists && ! $upgrade_dir_is_writable ) {
+ $result['status'] = 'critical';
+ $result['label'] = sprintf(
+ /* translators: %s: upgrade */
+ __( 'The %s directory exists but is not writable' ),
+ 'upgrade'
+ );
+ $result['description'] = sprintf(
+ /* translators: %s: wp-content/upgrade */
+ '' . __( 'The %s directory exists but is not writable. This directory is used for plugin and theme updates. Please make sure the server has write permissions to this directory.' ) . '
', + 'wp-content/upgrade'
+ );
+ return $result;
+ }
+
+ if ( ! $upgrade_dir_exists && ! $wp_filesystem->is_writable( $wp_content ) ) {
+ $result['status'] = 'critical';
+ $result['label'] = sprintf(
+ /* translators: %s: upgrade */
+ __( 'The %s directory cannot be created' ),
+ 'upgrade'
+ );
+ $result['description'] = sprintf(
+ /* translators: 1: wp-content/upgrade, 2: wp-content. */
+ '' . __( 'The %1$s directory does not exist, and the server does not have write permissions in %2$s to create it. This directory is used for plugin and theme updates. Please make sure the server has write permissions in %2$s.' ) . '
', + 'wp-content/upgrade',
+ 'wp-content'
+ );
+ return $result;
+ }
+
+ return $result;
+ }
+
/**
* Test if loopbacks work as expected.
*
@@ -2266,71 +2456,80 @@ class WP_Site_Health {
public static function get_tests() {
$tests = array(
'direct' => array(
- 'wordpress_version' => array(
+ 'wordpress_version' => array(
'label' => __( 'WordPress Version' ),
'test' => 'wordpress_version',
),
- 'plugin_version' => array(
+ 'plugin_version' => array(
'label' => __( 'Plugin Versions' ),
'test' => 'plugin_version',
),
- 'theme_version' => array(
+ 'theme_version' => array(
'label' => __( 'Theme Versions' ),
'test' => 'theme_version',
),
- 'php_version' => array(
+ 'php_version' => array(
'label' => __( 'PHP Version' ),
'test' => 'php_version',
),
- 'php_extensions' => array(
+ 'php_extensions' => array(
'label' => __( 'PHP Extensions' ),
'test' => 'php_extensions',
),
- 'php_default_timezone' => array(
+ 'php_default_timezone' => array(
'label' => __( 'PHP Default Timezone' ),
'test' => 'php_default_timezone',
),
- 'php_sessions' => array(
+ 'php_sessions' => array(
'label' => __( 'PHP Sessions' ),
'test' => 'php_sessions',
),
- 'sql_server' => array(
+ 'sql_server' => array(
'label' => __( 'Database Server version' ),
'test' => 'sql_server',
),
- 'utf8mb4_support' => array(
+ 'utf8mb4_support' => array(
'label' => __( 'MySQL utf8mb4 support' ),
'test' => 'utf8mb4_support',
),
- 'ssl_support' => array(
+ 'ssl_support' => array(
'label' => __( 'Secure communication' ),
'test' => 'ssl_support',
),
- 'scheduled_events' => array(
+ 'scheduled_events' => array(
'label' => __( 'Scheduled events' ),
'test' => 'scheduled_events',
),
- 'http_requests' => array(
+ 'http_requests' => array(
'label' => __( 'HTTP Requests' ),
'test' => 'http_requests',
),
- 'rest_availability' => array(
+ 'rest_availability' => array(
'label' => __( 'REST API availability' ),
'test' => 'rest_availability',
'skip_cron' => true,
),
- 'debug_enabled' => array(
+ 'debug_enabled' => array(
'label' => __( 'Debugging enabled' ),
'test' => 'is_in_debug_mode',
),
- 'file_uploads' => array(
+ 'file_uploads' => array(
'label' => __( 'File uploads' ),
'test' => 'file_uploads',
),
- 'plugin_theme_auto_updates' => array(
+ 'plugin_theme_auto_updates' => array(
'label' => __( 'Plugin and theme auto-updates' ),
'test' => 'plugin_theme_auto_updates',
),
+ 'update_temp_backup_writable' => array(
+ /* translators: %s: temp-backup */
+ 'label' => sprintf( __( 'Updates %s directory access' ), 'temp-backup' ),
+ 'test' => 'update_temp_backup_writable',
+ ),
+ 'available_updates_disk_space' => array(
+ 'label' => __( 'Available disk space' ),
+ 'test' => 'available_updates_disk_space',
+ ),
),
'async' => array(
'dotorg_communication' => array(
diff --git a/src/wp-admin/includes/class-wp-upgrader.php b/src/wp-admin/includes/class-wp-upgrader.php
index 72424cf407..2843d8dd0b 100644
--- a/src/wp-admin/includes/class-wp-upgrader.php
+++ b/src/wp-admin/includes/class-wp-upgrader.php
@@ -133,11 +133,25 @@ class WP_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 temp-backup directory.
+ *
* @since 2.8.0
+ * @since 5.9.0 Added the `schedule_temp_backup_cleanup()` task.
*/
public function init() {
$this->skin->set_upgrader( $this );
$this->generic_strings();
+ $this->schedule_temp_backup_cleanup();
+ }
+
+ /**
+ * Schedule cleanup of the temp-backup directory.
+ *
+ * @since 5.9.0
+ */
+ protected function schedule_temp_backup_cleanup() {
+ wp_schedule_event( time(), 'weekly', 'delete_temp_updater_backups' );
+ add_action( 'delete_temp_updater_backups', array( $this, 'delete_all_temp_backups' ) );
}
/**
@@ -166,6 +180,13 @@ class WP_Upgrader {
$this->strings['maintenance_start'] = __( 'Enabling Maintenance mode…' );
$this->strings['maintenance_end'] = __( 'Disabling Maintenance mode…' );
+
+ /* translators: %s: temp-backup */
+ $this->strings['temp_backup_mkdir_failed'] = sprintf( __( 'Could not create the %s directory.' ), 'temp-backup' );
+ /* translators: %s: temp-backup */
+ $this->strings['temp_backup_move_failed'] = sprintf( __( 'Could not move old version to the %s directory.' ), 'temp-backup' );
+ $this->strings['temp_backup_restore_failed'] = __( 'Could not restore original version.' );
+
}
/**
@@ -313,6 +334,9 @@ class WP_Upgrader {
$upgrade_files = $wp_filesystem->dirlist( $upgrade_folder );
if ( ! empty( $upgrade_files ) ) {
foreach ( $upgrade_files as $file ) {
+ if ( 'temp-backup' === $file['name'] ) {
+ continue;
+ }
$wp_filesystem->delete( $upgrade_folder . $file['name'], true );
}
}
@@ -493,6 +517,13 @@ class WP_Upgrader {
return $res;
}
+ 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;
+ }
+ }
+
// Retain the original source and destinations.
$remote_source = $args['source'];
$local_destination = $destination;
@@ -811,6 +842,9 @@ class WP_Upgrader {
$this->skin->set_result( $result );
if ( is_wp_error( $result ) ) {
+ if ( ! empty( $options['hook_extra']['temp_backup'] ) ) {
+ $this->restore_temp_backup( $options['hook_extra']['temp_backup'] );
+ }
$this->skin->error( $result );
if ( ! method_exists( $this->skin, 'hide_process_failed' ) || ! $this->skin->hide_process_failed( $result ) ) {
@@ -823,6 +857,11 @@ class WP_Upgrader {
$this->skin->after();
+ // Clean up the backup kept in the temp-backup directory.
+ if ( ! empty( $options['hook_extra']['temp_backup'] ) ) {
+ $this->delete_temp_backup( $options['hook_extra']['temp_backup'] );
+ }
+
if ( ! $options['is_multi'] ) {
/**
@@ -948,6 +987,154 @@ class WP_Upgrader {
return delete_option( $lock_name . '.lock' );
}
+ /**
+ * Moves the plugin/theme being updated into a temp-backup directory.
+ *
+ * @since 5.9.0
+ *
+ * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
+ *
+ * @param array $args Array of data for the temp-backup. Must include a slug, the source, and directory.
+ * @return bool|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;
+ }
+
+ $dest_dir = $wp_filesystem->wp_content_dir() . 'upgrade/temp-backup/';
+ // Create the temp-backup directory if it doesn't exist.
+ if ( (
+ ! $wp_filesystem->is_dir( $dest_dir )
+ && ! $wp_filesystem->mkdir( $dest_dir )
+ ) || (
+ ! $wp_filesystem->is_dir( $dest_dir . $args['dir'] . '/' )
+ && ! $wp_filesystem->mkdir( $dest_dir . $args['dir'] . '/' )
+ )
+ ) {
+ return new WP_Error( 'fs_temp_backup_mkdir', $this->strings['temp_backup_mkdir_failed'] );
+ }
+
+ $src = trailingslashit( $args['src'] ) . $args['slug'];
+ $dest = $dest_dir . $args['dir'] . '/' . $args['slug'];
+
+ // Delete the temp-backup directory if it already exists.
+ if ( $wp_filesystem->is_dir( $dest ) ) {
+ $wp_filesystem->delete( $dest, true );
+ }
+
+ // Move to the temp-backup directory.
+ if ( ! $wp_filesystem->move( $src, $dest, true ) ) {
+ return new WP_Error( 'fs_temp_backup_move', $this->strings['temp_backup_move_failed'] );
+ }
+
+ return true;
+ }
+
+ /**
+ * Restores the plugin/theme from the temp-backup directory.
+ *
+ * @since 5.9.0
+ *
+ * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
+ *
+ * @param array $args Array of data for the temp-backup. Must include a slug, the source, and directory.
+ * @return bool|WP_Error
+ */
+ public function restore_temp_backup( $args ) {
+ global $wp_filesystem;
+
+ if ( empty( $args['slug'] ) || empty( $args['src'] ) || empty( $args['dir'] ) ) {
+ return false;
+ }
+
+ $src = $wp_filesystem->wp_content_dir() . 'upgrade/temp-backup/' . $args['dir'] . '/' . $args['slug'];
+ $dest = trailingslashit( $args['src'] ) . $args['slug'];
+
+ if ( $wp_filesystem->is_dir( $src ) ) {
+ // Cleanup.
+ if ( $wp_filesystem->is_dir( $dest ) && ! $wp_filesystem->delete( $dest, true ) ) {
+ return new WP_Error( 'fs_temp_backup_delete', $this->strings['temp_backup_restore_failed'] );
+ }
+
+ // Move it.
+ if ( ! $wp_filesystem->move( $src, $dest, true ) ) {
+ return new WP_Error( 'fs_temp_backup_delete', $this->strings['temp_backup_restore_failed'] );
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Deletes a temp-backup.
+ *
+ * @since 5.9.0
+ *
+ * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
+ *
+ * @param array $args Array of data for the temp-backup. Must include a slug, the source, and directory.
+ * @return bool
+ */
+ public function delete_temp_backup( $args ) {
+ global $wp_filesystem;
+
+ if ( empty( $args['slug'] ) || empty( $args['dir'] ) ) {
+ return false;
+ }
+
+ return $wp_filesystem->delete(
+ $wp_filesystem->wp_content_dir() . "upgrade/temp-backup/{$args['dir']}/{$args['slug']}",
+ true
+ );
+ }
+
+ /**
+ * Deletes all contents of the temp-backup directory.
+ *
+ * @since 5.9.0
+ *
+ * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
+ */
+ public function delete_all_temp_backups() {
+ /*
+ * Check if there's a lock, or if currently performing an Ajax request,
+ * in which case there's a chance we're doing an update.
+ * Reschedule for an hour from now and exit early.
+ */
+ if ( get_option( 'core_updater.lock' ) || get_option( 'auto_updater.lock' ) || wp_doing_ajax() ) {
+ wp_schedule_single_event( time() + HOUR_IN_SECONDS, 'delete_temp_updater_backups' );
+ return;
+ }
+
+ add_action(
+ 'shutdown',
+ /*
+ * This action runs on shutdown to make sure there's no plugin updates currently running.
+ * Using a closure in this case is OK since the action can be removed by removing the parent hook.
+ */
+ function() {
+ global $wp_filesystem;
+
+ if ( ! $wp_filesystem ) {
+ include_once ABSPATH . '/wp-admin/includes/file.php';
+ WP_Filesystem();
+ }
+
+ $dirlist = $wp_filesystem->dirlist( $wp_filesystem->wp_content_dir() . 'upgrade/temp-backup/' );
+
+ foreach ( array_keys( $dirlist ) as $dir ) {
+ if ( '.' === $dir || '..' === $dir ) {
+ continue;
+ }
+
+ $wp_filesystem->delete( $wp_filesystem->wp_content_dir() . 'upgrade/temp-backup/' . $dir, true );
+ }
+ }
+ );
+ }
}
/** Plugin_Upgrader class */