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 */