wp/wp-admin/includes/class-wp-automatic-updater.php
changeset 16 a86126ab1dd4
parent 9 177826044cd9
child 18 be944660c56a
--- a/wp/wp-admin/includes/class-wp-automatic-updater.php	Tue Oct 22 16:11:46 2019 +0200
+++ b/wp/wp-admin/includes/class-wp-automatic-updater.php	Tue Dec 15 13:49:49 2020 +0100
@@ -73,7 +73,7 @@
 	 */
 	public function is_vcs_checkout( $context ) {
 		$context_dirs = array( untrailingslashit( $context ) );
-		if ( $context !== ABSPATH ) {
+		if ( ABSPATH !== $context ) {
 			$context_dirs[] = untrailingslashit( ABSPATH );
 		}
 
@@ -86,7 +86,7 @@
 				$check_dirs[] = $context_dir;
 
 				// Once we've hit '/' or 'C:\', we need to stop. dirname will keep returning the input here.
-				if ( $context_dir == dirname( $context_dir ) ) {
+				if ( dirname( $context_dir ) === $context_dir ) {
 					break;
 				}
 
@@ -99,7 +99,8 @@
 		// Search all directories we've found for evidence of version control.
 		foreach ( $vcs_dirs as $vcs_dir ) {
 			foreach ( $check_dirs as $check_dir ) {
-				if ( $checkout = @is_dir( rtrim( $check_dir, '\\/' ) . "/$vcs_dir" ) ) {
+				$checkout = @is_dir( rtrim( $check_dir, '\\/' ) . "/$vcs_dir" );
+				if ( $checkout ) {
 					break 2;
 				}
 			}
@@ -140,32 +141,50 @@
 			return false;
 		}
 
-		// Only relax the filesystem checks when the update doesn't include new files
+		// Only relax the filesystem checks when the update doesn't include new files.
 		$allow_relaxed_file_ownership = false;
-		if ( 'core' == $type && isset( $item->new_files ) && ! $item->new_files ) {
+		if ( 'core' === $type && isset( $item->new_files ) && ! $item->new_files ) {
 			$allow_relaxed_file_ownership = true;
 		}
 
 		// If we can't do an auto core update, we may still be able to email the user.
 		if ( ! $skin->request_filesystem_credentials( false, $context, $allow_relaxed_file_ownership ) || $this->is_vcs_checkout( $context ) ) {
-			if ( 'core' == $type ) {
+			if ( 'core' === $type ) {
 				$this->send_core_update_notification_email( $item );
 			}
 			return false;
 		}
 
 		// Next up, is this an item we can update?
-		if ( 'core' == $type ) {
+		if ( 'core' === $type ) {
 			$update = Core_Upgrader::should_update_to_version( $item->current );
+		} elseif ( 'plugin' === $type || 'theme' === $type ) {
+			$update = ! empty( $item->autoupdate );
+
+			if ( ! $update && wp_is_auto_update_enabled_for_type( $type ) ) {
+				// Check if the site admin has enabled auto-updates by default for the specific item.
+				$auto_updates = (array) get_site_option( "auto_update_{$type}s", array() );
+				$update       = in_array( $item->{$type}, $auto_updates, true );
+			}
 		} else {
 			$update = ! empty( $item->autoupdate );
 		}
 
+		// If the `disable_autoupdate` flag is set, override any user-choice, but allow filters.
+		if ( ! empty( $item->disable_autoupdate ) ) {
+			$update = $item->disable_autoupdate;
+		}
+
 		/**
 		 * Filters whether to automatically update core, a plugin, a theme, or a language.
 		 *
 		 * The dynamic portion of the hook name, `$type`, refers to the type of update
-		 * being checked. Can be 'core', 'theme', 'plugin', or 'translation'.
+		 * being checked. Potential hook names include:
+		 *
+		 *  - `auto_update_core`
+		 *  - `auto_update_plugin`
+		 *  - `auto_update_theme`
+		 *  - `auto_update_translation`
 		 *
 		 * Generally speaking, plugins, themes, and major core versions are not updated
 		 * by default, while translations and minor and development versions for core
@@ -176,21 +195,23 @@
 		 * adjust core updates.
 		 *
 		 * @since 3.7.0
+		 * @since 5.5.0 The `$update` parameter accepts the value of null.
 		 *
-		 * @param bool   $update Whether to update.
-		 * @param object $item   The update offer.
+		 * @param bool|null $update Whether to update. The value of null is internally used
+		 *                          to detect whether nothing has hooked into this filter.
+		 * @param object    $item   The update offer.
 		 */
 		$update = apply_filters( "auto_update_{$type}", $update, $item );
 
 		if ( ! $update ) {
-			if ( 'core' == $type ) {
+			if ( 'core' === $type ) {
 				$this->send_core_update_notification_email( $item );
 			}
 			return false;
 		}
 
 		// If it's a core update, are we actually compatible with its requirements?
-		if ( 'core' == $type ) {
+		if ( 'core' === $type ) {
 			global $wpdb;
 
 			$php_compat = version_compare( phpversion(), $item->php_version, '>=' );
@@ -205,8 +226,8 @@
 			}
 		}
 
-		// If updating a plugin, ensure the minimum PHP version requirements are satisfied.
-		if ( 'plugin' === $type ) {
+		// If updating a plugin or theme, ensure the minimum PHP version requirements are satisfied.
+		if ( in_array( $type, array( 'plugin', 'theme' ), true ) ) {
 			if ( ! empty( $item->requires_php ) && version_compare( phpversion(), $item->requires_php, '<' ) ) {
 				return false;
 			}
@@ -226,7 +247,7 @@
 		$notified = get_site_option( 'auto_core_update_notified' );
 
 		// Don't notify if we've already notified the same email address of the same version.
-		if ( $notified && $notified['email'] == get_site_option( 'admin_email' ) && $notified['version'] == $item->current ) {
+		if ( $notified && get_site_option( 'admin_email' ) === $notified['email'] && $notified['version'] == $item->current ) {
 			return false;
 		}
 
@@ -267,7 +288,6 @@
 	 *
 	 * @param string $type The type of update being checked: 'core', 'theme', 'plugin', 'translation'.
 	 * @param object $item The update offer.
-	 *
 	 * @return null|WP_Error
 	 */
 	public function update( $type, $item ) {
@@ -282,7 +302,7 @@
 				break;
 			case 'plugin':
 				$upgrader = new Plugin_Upgrader( $skin );
-				$context  = WP_PLUGIN_DIR; // We don't support custom Plugin directories, or updates for WPMU_PLUGIN_DIR
+				$context  = WP_PLUGIN_DIR; // We don't support custom Plugin directories, or updates for WPMU_PLUGIN_DIR.
 				break;
 			case 'theme':
 				$upgrader = new Theme_Upgrader( $skin );
@@ -314,40 +334,40 @@
 		$upgrader_item = $item;
 		switch ( $type ) {
 			case 'core':
-				/* translators: %s: WordPress version */
+				/* translators: %s: WordPress version. */
 				$skin->feedback( __( 'Updating to WordPress %s' ), $item->version );
-				/* translators: %s: WordPress version */
+				/* translators: %s: WordPress version. */
 				$item_name = sprintf( __( 'WordPress %s' ), $item->version );
 				break;
 			case 'theme':
 				$upgrader_item = $item->theme;
 				$theme         = wp_get_theme( $upgrader_item );
 				$item_name     = $theme->Get( 'Name' );
-				/* translators: %s: Theme name */
+				/* translators: %s: Theme name. */
 				$skin->feedback( __( 'Updating theme: %s' ), $item_name );
 				break;
 			case 'plugin':
 				$upgrader_item = $item->plugin;
 				$plugin_data   = get_plugin_data( $context . '/' . $upgrader_item );
 				$item_name     = $plugin_data['Name'];
-				/* translators: %s: Plugin name */
+				/* translators: %s: Plugin name. */
 				$skin->feedback( __( 'Updating plugin: %s' ), $item_name );
 				break;
 			case 'translation':
 				$language_item_name = $upgrader->get_name_for_update( $item );
-				/* translators: %s: Name of language item */
+				/* translators: %s: Project name (plugin, theme, or WordPress). */
 				$item_name = sprintf( __( 'Translations for %s' ), $language_item_name );
-				/* translators: 1: Name of language item, 2: Language */
+				/* translators: 1: Project name (plugin, theme, or WordPress), 2: Language. */
 				$skin->feedback( sprintf( __( 'Updating translations for %1$s (%2$s)&#8230;' ), $language_item_name, $item->language ) );
 				break;
 		}
 
 		$allow_relaxed_file_ownership = false;
-		if ( 'core' == $type && isset( $item->new_files ) && ! $item->new_files ) {
+		if ( 'core' === $type && isset( $item->new_files ) && ! $item->new_files ) {
 			$allow_relaxed_file_ownership = true;
 		}
 
-		// Boom, This sites about to get a whole new splash of paint!
+		// Boom, this site's about to get a whole new splash of paint!
 		$upgrade_result = $upgrader->upgrade(
 			$upgrader_item,
 			array(
@@ -356,7 +376,7 @@
 				'pre_check_md5'                => false,
 				// Only available for core updates.
 				'attempt_rollback'             => true,
-				// Allow relaxed file ownership in some scenarios
+				// Allow relaxed file ownership in some scenarios.
 				'allow_relaxed_file_ownership' => $allow_relaxed_file_ownership,
 			)
 		);
@@ -366,17 +386,21 @@
 			$upgrade_result = new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.' ) );
 		}
 
-		if ( 'core' == $type ) {
-			if ( is_wp_error( $upgrade_result ) && ( 'up_to_date' == $upgrade_result->get_error_code() || 'locked' == $upgrade_result->get_error_code() ) ) {
-				// These aren't actual errors, treat it as a skipped-update instead to avoid triggering the post-core update failure routines.
+		if ( 'core' === $type ) {
+			if ( is_wp_error( $upgrade_result )
+				&& ( 'up_to_date' === $upgrade_result->get_error_code()
+					|| 'locked' === $upgrade_result->get_error_code() )
+			) {
+				// These aren't actual errors, treat it as a skipped-update instead
+				// to avoid triggering the post-core update failure routines.
 				return false;
 			}
 
 			// Core doesn't output this, so let's append it so we don't get confused.
 			if ( is_wp_error( $upgrade_result ) ) {
-				$skin->error( __( 'Installation Failed' ), $upgrade_result );
+				$skin->error( __( 'Installation failed.' ), $upgrade_result );
 			} else {
-				$skin->feedback( __( 'WordPress updated successfully' ) );
+				$skin->feedback( __( 'WordPress updated successfully.' ) );
 			}
 		}
 
@@ -408,51 +432,51 @@
 			return;
 		}
 
-		// Don't automatically run these thins, as we'll handle it ourselves
+		// Don't automatically run these things, as we'll handle it ourselves.
 		remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 );
 		remove_action( 'upgrader_process_complete', 'wp_version_check' );
 		remove_action( 'upgrader_process_complete', 'wp_update_plugins' );
 		remove_action( 'upgrader_process_complete', 'wp_update_themes' );
 
-		// Next, Plugins
-		wp_update_plugins(); // Check for Plugin updates
+		// Next, plugins.
+		wp_update_plugins(); // Check for plugin updates.
 		$plugin_updates = get_site_transient( 'update_plugins' );
 		if ( $plugin_updates && ! empty( $plugin_updates->response ) ) {
 			foreach ( $plugin_updates->response as $plugin ) {
 				$this->update( 'plugin', $plugin );
 			}
-			// Force refresh of plugin update information
+			// Force refresh of plugin update information.
 			wp_clean_plugins_cache();
 		}
 
-		// Next, those themes we all love
-		wp_update_themes();  // Check for Theme updates
+		// Next, those themes we all love.
+		wp_update_themes();  // Check for theme updates.
 		$theme_updates = get_site_transient( 'update_themes' );
 		if ( $theme_updates && ! empty( $theme_updates->response ) ) {
 			foreach ( $theme_updates->response as $theme ) {
 				$this->update( 'theme', (object) $theme );
 			}
-			// Force refresh of theme update information
+			// Force refresh of theme update information.
 			wp_clean_themes_cache();
 		}
 
-		// Next, Process any core update
-		wp_version_check(); // Check for Core updates
+		// Next, process any core update.
+		wp_version_check(); // Check for core updates.
 		$core_update = find_core_auto_update();
 
 		if ( $core_update ) {
 			$this->update( 'core', $core_update );
 		}
 
-		// Clean up, and check for any pending translations
-		// (Core_Upgrader checks for core updates)
+		// Clean up, and check for any pending translations.
+		// (Core_Upgrader checks for core updates.)
 		$theme_stats = array();
 		if ( isset( $this->update_results['theme'] ) ) {
 			foreach ( $this->update_results['theme'] as $upgrade ) {
 				$theme_stats[ $upgrade->item->theme ] = ( true === $upgrade->result );
 			}
 		}
-		wp_update_themes( $theme_stats );  // Check for Theme updates
+		wp_update_themes( $theme_stats ); // Check for theme updates.
 
 		$plugin_stats = array();
 		if ( isset( $this->update_results['plugin'] ) ) {
@@ -460,21 +484,21 @@
 				$plugin_stats[ $upgrade->item->plugin ] = ( true === $upgrade->result );
 			}
 		}
-		wp_update_plugins( $plugin_stats ); // Check for Plugin updates
+		wp_update_plugins( $plugin_stats ); // Check for plugin updates.
 
-		// Finally, Process any new translations
+		// Finally, process any new translations.
 		$language_updates = wp_get_translation_updates();
 		if ( $language_updates ) {
 			foreach ( $language_updates as $update ) {
 				$this->update( 'translation', $update );
 			}
 
-			// Clear existing caches
+			// Clear existing caches.
 			wp_clean_update_cache();
 
-			wp_version_check();  // check for Core updates
-			wp_update_themes();  // Check for Theme updates
-			wp_update_plugins(); // Check for Plugin updates
+			wp_version_check();  // Check for core updates.
+			wp_update_themes();  // Check for theme updates.
+			wp_update_plugins(); // Check for plugin updates.
 		}
 
 		// Send debugging email to admin for all development installations.
@@ -496,6 +520,8 @@
 
 			if ( ! empty( $this->update_results['core'] ) ) {
 				$this->after_core_update( $this->update_results['core'][0] );
+			} elseif ( ! empty( $this->update_results['plugin'] ) || ! empty( $this->update_results['theme'] ) ) {
+				$this->after_plugin_theme_update( $this->update_results );
 			}
 
 			/**
@@ -535,9 +561,9 @@
 		// Any of these WP_Error codes are critical failures, as in they occurred after we started to copy core files.
 		// We should not try to perform a background update again until there is a successful one-click update performed by the user.
 		$critical = false;
-		if ( $error_code === 'disk_full' || false !== strpos( $error_code, '__copy_dir' ) ) {
+		if ( 'disk_full' === $error_code || false !== strpos( $error_code, '__copy_dir' ) ) {
 			$critical = true;
-		} elseif ( $error_code === 'rollback_was_required' && is_wp_error( $result->get_error_data()->rollback ) ) {
+		} elseif ( 'rollback_was_required' === $error_code && is_wp_error( $result->get_error_data()->rollback ) ) {
 			// A rollback is only critical if it failed too.
 			$critical        = true;
 			$rollback_result = $result->get_error_data()->rollback;
@@ -576,14 +602,14 @@
 		 */
 		$send               = true;
 		$transient_failures = array( 'incompatible_archive', 'download_failed', 'insane_distro', 'locked' );
-		if ( in_array( $error_code, $transient_failures ) && ! get_site_option( 'auto_core_update_failed' ) ) {
+		if ( in_array( $error_code, $transient_failures, true ) && ! get_site_option( 'auto_core_update_failed' ) ) {
 			wp_schedule_single_event( time() + HOUR_IN_SECONDS, 'wp_maybe_auto_update' );
 			$send = false;
 		}
 
 		$n = get_site_option( 'auto_core_update_notified' );
 		// Don't notify if we've already notified the same email address of the same version of the same notification type.
-		if ( $n && 'fail' == $n['type'] && $n['email'] == get_site_option( 'admin_email' ) && $n['version'] == $core_update->current ) {
+		if ( $n && 'fail' === $n['type'] && get_site_option( 'admin_email' ) === $n['email'] && $n['version'] == $core_update->current ) {
 			$send = false;
 		}
 
@@ -595,7 +621,7 @@
 				'error_code' => $error_code,
 				'error_data' => $result->get_error_data(),
 				'timestamp'  => time(),
-				'retry'      => in_array( $error_code, $transient_failures ),
+				'retry'      => in_array( $error_code, $transient_failures, true ),
 			)
 		);
 
@@ -625,11 +651,13 @@
 		);
 
 		$next_user_core_update = get_preferred_from_update_core();
-		// If the update transient is empty, use the update we just performed
+
+		// If the update transient is empty, use the update we just performed.
 		if ( ! $next_user_core_update ) {
 			$next_user_core_update = $core_update;
 		}
-		$newer_version_available = ( 'upgrade' == $next_user_core_update->response && version_compare( $next_user_core_update->version, $core_update->version, '>' ) );
+
+		$newer_version_available = ( 'upgrade' === $next_user_core_update->response && version_compare( $next_user_core_update->version, $core_update->version, '>' ) );
 
 		/**
 		 * Filters whether to send an email following an automatic background core update.
@@ -648,13 +676,13 @@
 
 		switch ( $type ) {
 			case 'success': // We updated.
-				/* translators: Site updated notification email subject. 1: Site title, 2: WordPress version number. */
+				/* translators: Site updated notification email subject. 1: Site title, 2: WordPress version. */
 				$subject = __( '[%1$s] Your site has updated to WordPress %2$s' );
 				break;
 
 			case 'fail':   // We tried to update but couldn't.
 			case 'manual': // We can't update (and made no attempt).
-				/* translators: Update available notification email subject. 1: Site title, 2: WordPress version number. */
+				/* translators: Update available notification email subject. 1: Site title, 2: WordPress version. */
 				$subject = __( '[%1$s] WordPress %2$s is available. Please update!' );
 				break;
 
@@ -667,7 +695,7 @@
 				return;
 		}
 
-		// If the auto update is not to the latest version, say that the current version of WP is available instead.
+		// If the auto-update is not to the latest version, say that the current version of WP is available instead.
 		$version = 'success' === $type ? $core_update->current : $next_user_core_update->current;
 		$subject = sprintf( $subject, wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ), $version );
 
@@ -675,8 +703,12 @@
 
 		switch ( $type ) {
 			case 'success':
-				/* translators: 1: Home URL, 2: WordPress version */
-				$body .= sprintf( __( 'Howdy! Your site at %1$s has been updated automatically to WordPress %2$s.' ), home_url(), $core_update->current );
+				$body .= sprintf(
+					/* translators: 1: Home URL, 2: WordPress version. */
+					__( 'Howdy! Your site at %1$s has been updated automatically to WordPress %2$s.' ),
+					home_url(),
+					$core_update->current
+				);
 				$body .= "\n\n";
 				if ( ! $newer_version_available ) {
 					$body .= __( 'No further action is needed on your part.' ) . ' ';
@@ -684,12 +716,12 @@
 
 				// Can only reference the About screen if their update was successful.
 				list( $about_version ) = explode( '-', $core_update->current, 2 );
-				/* translators: %s: WordPress core version */
+				/* translators: %s: WordPress version. */
 				$body .= sprintf( __( 'For more on version %s, see the About WordPress screen:' ), $about_version );
 				$body .= "\n" . admin_url( 'about.php' );
 
 				if ( $newer_version_available ) {
-					/* translators: %s: WordPress core latest version */
+					/* translators: %s: WordPress latest version. */
 					$body .= "\n\n" . sprintf( __( 'WordPress %s is also now available.' ), $next_user_core_update->current ) . ' ';
 					$body .= __( 'Updating is easy and only takes a few moments:' );
 					$body .= "\n" . network_admin_url( 'update-core.php' );
@@ -699,14 +731,18 @@
 
 			case 'fail':
 			case 'manual':
-				/* translators: 1: Home URL, 2: WordPress core latest version */
-				$body .= sprintf( __( 'Please update your site at %1$s to WordPress %2$s.' ), home_url(), $next_user_core_update->current );
+				$body .= sprintf(
+					/* translators: 1: Home URL, 2: WordPress version. */
+					__( 'Please update your site at %1$s to WordPress %2$s.' ),
+					home_url(),
+					$next_user_core_update->current
+				);
 
 				$body .= "\n\n";
 
 				// Don't show this message if there is a newer version available.
 				// Potential for confusion, and also not useful for them to know at this point.
-				if ( 'fail' == $type && ! $newer_version_available ) {
+				if ( 'fail' === $type && ! $newer_version_available ) {
 					$body .= __( 'We tried but were unable to update your site automatically.' ) . ' ';
 				}
 
@@ -716,11 +752,19 @@
 
 			case 'critical':
 				if ( $newer_version_available ) {
-					/* translators: 1: Home URL, 2: WordPress core latest version */
-					$body .= sprintf( __( 'Your site at %1$s experienced a critical failure while trying to update WordPress to version %2$s.' ), home_url(), $core_update->current );
+					$body .= sprintf(
+						/* translators: 1: Home URL, 2: WordPress version. */
+						__( 'Your site at %1$s experienced a critical failure while trying to update WordPress to version %2$s.' ),
+						home_url(),
+						$core_update->current
+					);
 				} else {
-					/* translators: 1: Home URL, 2: Core update version */
-					$body .= sprintf( __( 'Your site at %1$s experienced a critical failure while trying to update to the latest version of WordPress, %2$s.' ), home_url(), $core_update->current );
+					$body .= sprintf(
+						/* translators: 1: Home URL, 2: WordPress latest version. */
+						__( 'Your site at %1$s experienced a critical failure while trying to update to the latest version of WordPress, %2$s.' ),
+						home_url(),
+						$core_update->current
+					);
 				}
 
 				$body .= "\n\n" . __( "This means your site may be offline or broken. Don't panic; this can be fixed." );
@@ -733,16 +777,19 @@
 		$critical_support = 'critical' === $type && ! empty( $core_update->support_email );
 		if ( $critical_support ) {
 			// Support offer if available.
-			/* translators: %s: Support e-mail */
-			$body .= "\n\n" . sprintf( __( 'The WordPress team is willing to help you. Forward this email to %s and the team will work with you to make sure your site is working.' ), $core_update->support_email );
+			$body .= "\n\n" . sprintf(
+				/* translators: %s: Support email address. */
+				__( 'The WordPress team is willing to help you. Forward this email to %s and the team will work with you to make sure your site is working.' ),
+				$core_update->support_email
+			);
 		} else {
 			// Add a note about the support forums.
 			$body .= "\n\n" . __( 'If you experience any issues or need support, the volunteers in the WordPress.org support forums may be able to help.' );
-			$body .= "\n" . __( 'https://wordpress.org/support/' );
+			$body .= "\n" . __( 'https://wordpress.org/support/forums/' );
 		}
 
 		// Updates are important!
-		if ( $type != 'success' || $newer_version_available ) {
+		if ( 'success' !== $type || $newer_version_available ) {
 			$body .= "\n\n" . __( 'Keeping your site updated is important for security. It also makes the internet a safer place for you and your readers.' );
 		}
 
@@ -751,23 +798,23 @@
 		}
 
 		// If things are successful and we're now on the latest, mention plugins and themes if any are out of date.
-		if ( $type == 'success' && ! $newer_version_available && ( get_plugin_updates() || get_theme_updates() ) ) {
+		if ( 'success' === $type && ! $newer_version_available && ( get_plugin_updates() || get_theme_updates() ) ) {
 			$body .= "\n\n" . __( 'You also have some plugins or themes with updates available. Update them now:' );
 			$body .= "\n" . network_admin_url();
 		}
 
 		$body .= "\n\n" . __( 'The WordPress Team' ) . "\n";
 
-		if ( 'critical' == $type && is_wp_error( $result ) ) {
+		if ( 'critical' === $type && is_wp_error( $result ) ) {
 			$body .= "\n***\n\n";
-			/* translators: %s: WordPress version */
+			/* translators: %s: WordPress version. */
 			$body .= sprintf( __( 'Your site was running version %s.' ), get_bloginfo( 'version' ) );
 			$body .= ' ' . __( 'We have some data that describes the error your site encountered.' );
 			$body .= ' ' . __( 'Your hosting company, support forum volunteers, or a friendly developer may be able to use this information to help you:' );
 
 			// If we had a rollback and we're still critical, then the rollback failed too.
 			// Loop through all errors (the main WP_Error, the update result, the rollback result) for code, data, etc.
-			if ( 'rollback_was_required' == $result->get_error_code() ) {
+			if ( 'rollback_was_required' === $result->get_error_code() ) {
 				$errors = array( $result, $result->get_error_data()->update, $result->get_error_data()->rollback );
 			} else {
 				$errors = array( $result );
@@ -777,20 +824,25 @@
 				if ( ! is_wp_error( $error ) ) {
 					continue;
 				}
+
 				$error_code = $error->get_error_code();
-				/* translators: %s: Error code */
+				/* translators: %s: Error code. */
 				$body .= "\n\n" . sprintf( __( 'Error code: %s' ), $error_code );
-				if ( 'rollback_was_required' == $error_code ) {
+
+				if ( 'rollback_was_required' === $error_code ) {
 					continue;
 				}
+
 				if ( $error->get_error_message() ) {
 					$body .= "\n" . $error->get_error_message();
 				}
+
 				$error_data = $error->get_error_data();
 				if ( $error_data ) {
 					$body .= "\n" . implode( ', ', (array) $error_data );
 				}
 			}
+
 			$body .= "\n";
 		}
 
@@ -823,6 +875,322 @@
 		wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] );
 	}
 
+
+	/**
+	 * If we tried to perform plugin or theme updates, check if we should send an email.
+	 *
+	 * @since 5.5.0
+	 *
+	 * @param array $update_results The results of update tasks.
+	 */
+	protected function after_plugin_theme_update( $update_results ) {
+		$successful_updates = array();
+		$failed_updates     = array();
+
+		/**
+		 * Filters whether to send an email following an automatic background plugin update.
+		 *
+		 * @since 5.5.0
+		 * @since 5.5.1 Added the $update_results parameter.
+		 *
+		 * @param bool  $enabled        True if plugins notifications are enabled, false otherwise.
+		 * @param array $update_results The results of plugins update tasks.
+		 */
+		$notifications_enabled = apply_filters( 'auto_plugin_update_send_email', true, $update_results['plugin'] );
+
+		if ( ! empty( $update_results['plugin'] ) && $notifications_enabled ) {
+			foreach ( $update_results['plugin'] as $update_result ) {
+				if ( true === $update_result->result ) {
+					$successful_updates['plugin'][] = $update_result;
+				} else {
+					$failed_updates['plugin'][] = $update_result;
+				}
+			}
+		}
+
+		/**
+		 * Filters whether to send an email following an automatic background theme update.
+		 *
+		 * @since 5.5.0
+		 * @since 5.5.1 Added the $update_results parameter.
+		 *
+		 * @param bool  $enabled True if notifications are enabled, false otherwise.
+		 * @param array $update_results The results of theme update tasks.
+		 */
+		$notifications_enabled = apply_filters( 'auto_theme_update_send_email', true, $update_results['theme'] );
+
+		if ( ! empty( $update_results['theme'] ) && $notifications_enabled ) {
+			foreach ( $update_results['theme'] as $update_result ) {
+				if ( true === $update_result->result ) {
+					$successful_updates['theme'][] = $update_result;
+				} else {
+					$failed_updates['theme'][] = $update_result;
+				}
+			}
+		}
+
+		if ( empty( $successful_updates ) && empty( $failed_updates ) ) {
+			return;
+		}
+
+		if ( empty( $failed_updates ) ) {
+			$this->send_plugin_theme_email( 'success', $successful_updates, $failed_updates );
+		} elseif ( empty( $successful_updates ) ) {
+			$this->send_plugin_theme_email( 'fail', $successful_updates, $failed_updates );
+		} else {
+			$this->send_plugin_theme_email( 'mixed', $successful_updates, $failed_updates );
+		}
+	}
+
+	/**
+	 * Sends an email upon the completion or failure of a plugin or theme background update.
+	 *
+	 * @since 5.5.0
+	 *
+	 * @param string $type               The type of email to send. Can be one of 'success', 'fail', 'mixed'.
+	 * @param array  $successful_updates A list of updates that succeeded.
+	 * @param array  $failed_updates     A list of updates that failed.
+	 */
+	protected function send_plugin_theme_email( $type, $successful_updates, $failed_updates ) {
+		// No updates were attempted.
+		if ( empty( $successful_updates ) && empty( $failed_updates ) ) {
+			return;
+		}
+
+		$unique_failures     = false;
+		$past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() );
+
+		/*
+		 * When only failures have occurred, an email should only be sent if there are unique failures.
+		 * A failure is considered unique if an email has not been sent for an update attempt failure
+		 * to a plugin or theme with the same new_version.
+		 */
+		if ( 'fail' === $type ) {
+			foreach ( $failed_updates as $update_type => $failures ) {
+				foreach ( $failures as $failed_update ) {
+					if ( ! isset( $past_failure_emails[ $failed_update->item->{$update_type} ] ) ) {
+						$unique_failures = true;
+						continue;
+					}
+
+					// Check that the failure represents a new failure based on the new_version.
+					if ( version_compare( $past_failure_emails[ $failed_update->item->{$update_type} ], $failed_update->item->new_version, '<' ) ) {
+						$unique_failures = true;
+					}
+				}
+			}
+
+			if ( ! $unique_failures ) {
+				return;
+			}
+		}
+
+		$body               = array();
+		$successful_plugins = ( ! empty( $successful_updates['plugin'] ) );
+		$successful_themes  = ( ! empty( $successful_updates['theme'] ) );
+		$failed_plugins     = ( ! empty( $failed_updates['plugin'] ) );
+		$failed_themes      = ( ! empty( $failed_updates['theme'] ) );
+
+		switch ( $type ) {
+			case 'success':
+				if ( $successful_plugins && $successful_themes ) {
+					/* translators: %s: Site title. */
+					$subject = __( '[%s] Some plugins and themes have automatically updated' );
+					$body[]  = sprintf(
+						/* translators: %s: Home URL. */
+						__( 'Howdy! Some plugins and themes have automatically updated to their latest versions on your site at %s. No further action is needed on your part.' ),
+						home_url()
+					);
+				} elseif ( $successful_plugins ) {
+					/* translators: %s: Site title. */
+					$subject = __( '[%s] Some plugins were automatically updated' );
+					$body[]  = sprintf(
+						/* translators: %s: Home URL. */
+						__( 'Howdy! Some plugins have automatically updated to their latest versions on your site at %s. No further action is needed on your part.' ),
+						home_url()
+					);
+				} else {
+					/* translators: %s: Site title. */
+					$subject = __( '[%s] Some themes were automatically updated' );
+					$body[]  = sprintf(
+						/* translators: %s: Home URL. */
+						__( 'Howdy! Some themes have automatically updated to their latest versions on your site at %s. No further action is needed on your part.' ),
+						home_url()
+					);
+				}
+
+				break;
+			case 'fail':
+			case 'mixed':
+				if ( $failed_plugins && $failed_themes ) {
+					/* translators: %s: Site title. */
+					$subject = __( '[%s] Some plugins and themes have failed to update' );
+					$body[]  = sprintf(
+						/* translators: %s: Home URL. */
+						__( 'Howdy! Plugins and themes failed to update on your site at %s.' ),
+						home_url()
+					);
+				} elseif ( $failed_plugins ) {
+					/* translators: %s: Site title. */
+					$subject = __( '[%s] Some plugins have failed to update' );
+					$body[]  = sprintf(
+						/* translators: %s: Home URL. */
+						__( 'Howdy! Plugins failed to update on your site at %s.' ),
+						home_url()
+					);
+				} else {
+					/* translators: %s: Site title. */
+					$subject = __( '[%s] Some themes have failed to update' );
+					$body[]  = sprintf(
+						/* translators: %s: Home URL. */
+						__( 'Howdy! Themes failed to update on your site at %s.' ),
+						home_url()
+					);
+				}
+
+				break;
+		}
+
+		if ( in_array( $type, array( 'fail', 'mixed' ), true ) ) {
+			$body[] = "\n";
+			$body[] = __( 'Please check your site now. It’s possible that everything is working. If there are updates available, you should update.' );
+			$body[] = "\n";
+
+			// List failed plugin updates.
+			if ( ! empty( $failed_updates['plugin'] ) ) {
+				$body[] = __( 'These plugins failed to update:' );
+
+				foreach ( $failed_updates['plugin'] as $item ) {
+					$body[] = sprintf(
+						/* translators: 1: Plugin name, 2: Version number. */
+						__( '- %1$s version %2$s' ),
+						$item->name,
+						$item->item->new_version
+					);
+
+					$past_failure_emails[ $item->item->plugin ] = $item->item->new_version;
+				}
+
+				$body[] = "\n";
+			}
+
+			// List failed theme updates.
+			if ( ! empty( $failed_updates['theme'] ) ) {
+				$body[] = __( 'These themes failed to update:' );
+
+				foreach ( $failed_updates['theme'] as $item ) {
+					$body[] = sprintf(
+						/* translators: 1: Theme name, 2: Version number. */
+						__( '- %1$s version %2$s' ),
+						$item->name,
+						$item->item->new_version
+					);
+
+					$past_failure_emails[ $item->item->theme ] = $item->item->new_version;
+				}
+
+				$body[] = "\n";
+			}
+		}
+
+		// List successful updates.
+		if ( in_array( $type, array( 'success', 'mixed' ), true ) ) {
+			$body[] = "\n";
+
+			// List successful plugin updates.
+			if ( ! empty( $successful_updates['plugin'] ) ) {
+				$body[] = __( 'These plugins are now up to date:' );
+
+				foreach ( $successful_updates['plugin'] as $item ) {
+					$body[] = sprintf(
+						/* translators: 1: Plugin name, 2: Version number. */
+						__( '- %1$s version %2$s' ),
+						$item->name,
+						$item->item->new_version
+					);
+
+					unset( $past_failure_emails[ $item->item->plugin ] );
+				}
+
+				$body[] = "\n";
+			}
+
+			// List successful theme updates.
+			if ( ! empty( $successful_updates['theme'] ) ) {
+				$body[] = __( 'These themes are now up to date:' );
+
+				foreach ( $successful_updates['theme'] as $item ) {
+					$body[] = sprintf(
+						/* translators: 1: Theme name, 2: Version number. */
+						__( '- %1$s version %2$s' ),
+						$item->name,
+						$item->item->new_version
+					);
+
+					unset( $past_failure_emails[ $item->item->theme ] );
+				}
+
+				$body[] = "\n";
+			}
+		}
+
+		if ( $failed_plugins ) {
+			$body[] = sprintf(
+				/* translators: %s: Plugins screen URL. */
+				__( 'To manage plugins on your site, visit the Plugins page: %s' ),
+				admin_url( 'plugins.php' )
+			);
+			$body[] = "\n";
+		}
+
+		if ( $failed_themes ) {
+			$body[] = sprintf(
+				/* translators: %s: Themes screen URL. */
+				__( 'To manage themes on your site, visit the Themes page: %s' ),
+				admin_url( 'themes.php' )
+			);
+			$body[] = "\n";
+		}
+
+		// Add a note about the support forums.
+		$body[] = __( 'If you experience any issues or need support, the volunteers in the WordPress.org support forums may be able to help.' );
+		$body[] = __( 'https://wordpress.org/support/forums/' );
+		$body[] = "\n" . __( 'The WordPress Team' );
+
+		$body    = implode( "\n", $body );
+		$to      = get_site_option( 'admin_email' );
+		$subject = sprintf( $subject, wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) );
+		$headers = '';
+
+		$email = compact( 'to', 'subject', 'body', 'headers' );
+
+		/**
+		 * Filters the email sent following an automatic background update for plugins and themes.
+		 *
+		 * @since 5.5.0
+		 *
+		 * @param array  $email {
+		 *     Array of email arguments that will be passed to wp_mail().
+		 *
+		 *     @type string $to      The email recipient. An array of emails
+		 *                           can be returned, as handled by wp_mail().
+		 *     @type string $subject The email's subject.
+		 *     @type string $body    The email message body.
+		 *     @type string $headers Any email headers, defaults to no headers.
+		 * }
+		 * @param string $type               The type of email being sent. Can be one of 'success', 'fail', 'mixed'.
+		 * @param array  $successful_updates A list of updates that succeeded.
+		 * @param array  $failed_updates     A list of updates that failed.
+		 */
+		$email = apply_filters( 'auto_plugin_theme_update_email', $email, $type, $successful_updates, $failed_updates );
+
+		$result = wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] );
+
+		if ( $result ) {
+			update_option( 'auto_plugin_theme_update_emails', $past_failure_emails );
+		}
+	}
+
 	/**
 	 * Prepares and sends an email of a full log of background update results, useful for debugging and geekery.
 	 *
@@ -837,24 +1205,24 @@
 		$body     = array();
 		$failures = 0;
 
-		/* translators: %s: Network home URL */
+		/* translators: %s: Network home URL. */
 		$body[] = sprintf( __( 'WordPress site: %s' ), network_home_url( '/' ) );
 
-		// Core
+		// Core.
 		if ( isset( $this->update_results['core'] ) ) {
 			$result = $this->update_results['core'][0];
 			if ( $result->result && ! is_wp_error( $result->result ) ) {
-				/* translators: %s: WordPress core version */
+				/* translators: %s: WordPress version. */
 				$body[] = sprintf( __( 'SUCCESS: WordPress was successfully updated to %s' ), $result->name );
 			} else {
-				/* translators: %s: WordPress core version */
+				/* translators: %s: WordPress version. */
 				$body[] = sprintf( __( 'FAILED: WordPress failed to update to %s' ), $result->name );
 				$failures++;
 			}
 			$body[] = '';
 		}
 
-		// Plugins, Themes, Translations
+		// Plugins, Themes, Translations.
 		foreach ( array( 'plugin', 'theme', 'translation' ) as $type ) {
 			if ( ! isset( $this->update_results[ $type ] ) ) {
 				continue;
@@ -869,12 +1237,12 @@
 
 				$body[] = $messages[ $type ];
 				foreach ( wp_list_pluck( $success_items, 'name' ) as $name ) {
-					/* translators: %s: name of plugin / theme / translations */
+					/* translators: %s: Name of plugin / theme / translation. */
 					$body[] = ' * ' . sprintf( __( 'SUCCESS: %s' ), $name );
 				}
 			}
 			if ( $success_items != $this->update_results[ $type ] ) {
-				// Failed updates
+				// Failed updates.
 				$messages = array(
 					'plugin'      => __( 'The following plugins failed to update:' ),
 					'theme'       => __( 'The following themes failed to update:' ),
@@ -884,7 +1252,7 @@
 				$body[] = $messages[ $type ];
 				foreach ( $this->update_results[ $type ] as $item ) {
 					if ( ! $item->result || is_wp_error( $item->result ) ) {
-						/* translators: %s: name of plugin / theme / translations */
+						/* translators: %s: Name of plugin / theme / translation. */
 						$body[] = ' * ' . sprintf( __( 'FAILED: %s' ), $item->name );
 						$failures++;
 					}
@@ -911,10 +1279,10 @@
 			);
 			$body[] = '';
 
-			/* translators: Background update failed notification email subject. %s: Site title */
+			/* translators: Background update failed notification email subject. %s: Site title. */
 			$subject = sprintf( __( '[%s] Background Update Failed' ), $site_title );
 		} else {
-			/* translators: Background update finished notification email subject. %s: Site title */
+			/* translators: Background update finished notification email subject. %s: Site title. */
 			$subject = sprintf( __( '[%s] Background Update Finished' ), $site_title );
 		}