diff --git a/src/wp-admin/includes/class-plugin-upgrader.php b/src/wp-admin/includes/class-plugin-upgrader.php index 56417a58e0..689206b637 100644 --- a/src/wp-admin/includes/class-plugin-upgrader.php +++ b/src/wp-admin/includes/class-plugin-upgrader.php @@ -247,6 +247,15 @@ class Plugin_Upgrader extends WP_Upgrader { // Force refresh of plugin update information. wp_clean_plugins_cache( $parsed_args['clear_update_cache'] ); + // Ensure any future auto-update failures trigger a failure email by removing the last + // failure notification from the list when plugins update successfully. + $past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() ); + + if ( isset( $past_failure_emails[ $plugin ] ) ) { + unset( $past_failure_emails[ $plugin ] ); + update_option( 'auto_plugin_theme_update_emails', $past_failure_emails ); + } + return true; } @@ -370,6 +379,20 @@ class Plugin_Upgrader extends WP_Upgrader { // Cleanup our hooks, in case something else does a upgrade on this connection. remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ) ); + // Ensure any future auto-update failures trigger a failure email by removing the last + // failure notification from the list when plugins update successfully. + $past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() ); + + foreach ( $results as $plugin => $result ) { + // Maintain last failure notification when plugins failed to update manually. + if ( ! $result || is_wp_error( $result ) || ! isset( $past_failure_emails[ $plugin ] ) ) { + continue; + } + + unset( $past_failure_emails[ $plugin ] ); + } + update_option( 'auto_plugin_theme_update_emails', $past_failure_emails ); + return $results; } diff --git a/src/wp-admin/includes/class-theme-upgrader.php b/src/wp-admin/includes/class-theme-upgrader.php index b26498b3e9..020c49469e 100644 --- a/src/wp-admin/includes/class-theme-upgrader.php +++ b/src/wp-admin/includes/class-theme-upgrader.php @@ -352,6 +352,15 @@ class Theme_Upgrader extends WP_Upgrader { wp_clean_themes_cache( $parsed_args['clear_update_cache'] ); + // Ensure any future auto-update failures trigger a failure email by removing the last + // failure notification from the list when themes update successfully. + $past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() ); + + if ( isset( $past_failure_emails[ $theme ] ) ) { + unset( $past_failure_emails[ $theme ] ); + update_option( 'auto_plugin_theme_update_emails', $past_failure_emails ); + } + return true; } @@ -479,6 +488,20 @@ class Theme_Upgrader extends WP_Upgrader { remove_filter( 'upgrader_post_install', array( $this, 'current_after' ) ); remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_theme' ) ); + // Ensure any future auto-update failures trigger a failure email by removing the last + // failure notification from the list when themes update successfully. + $past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() ); + + foreach ( $results as $theme => $result ) { + // Maintain last failure notification when themes failed to update manually. + if ( ! $result || is_wp_error( $result ) || ! isset( $past_failure_emails[ $theme ] ) ) { + continue; + } + + unset( $past_failure_emails[ $theme ] ); + } + update_option( 'auto_plugin_theme_update_emails', $past_failure_emails ); + return $results; } diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index b5b47f06c5..f94f139e50 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -941,6 +941,32 @@ class WP_Automatic_Updater { 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'] ) ); @@ -1017,7 +1043,8 @@ class WP_Automatic_Updater { $body[] = __( 'These plugins failed to update:' ); foreach ( $failed_updates['plugin'] as $item ) { - $body[] = "- {$item->name}"; + $body[] = "- {$item->name}"; + $past_failure_emails[ $item->item->plugin ] = $item->item->new_version; } $body[] = "\n"; } @@ -1027,7 +1054,8 @@ class WP_Automatic_Updater { $body[] = __( 'These themes failed to update:' ); foreach ( $failed_updates['theme'] as $item ) { - $body[] = "- {$item->name}"; + $body[] = "- {$item->name}"; + $past_failure_emails[ $item->item->theme ] = $item->item->new_version; } $body[] = "\n"; } @@ -1043,6 +1071,7 @@ class WP_Automatic_Updater { foreach ( $successful_updates['plugin'] as $item ) { $body[] = "- {$item->name}"; + unset( $past_failure_emails[ $item->item->plugin ] ); } $body[] = "\n"; } @@ -1053,6 +1082,7 @@ class WP_Automatic_Updater { // List successful updates. foreach ( $successful_updates['theme'] as $item ) { $body[] = "- {$item->name}"; + unset( $past_failure_emails[ $item->item->theme ] ); } $body[] = "\n"; } @@ -1108,7 +1138,11 @@ class WP_Automatic_Updater { */ $email = apply_filters( 'auto_plugin_theme_update_email', $email, $type, $successful_updates, $failed_updates ); - wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] ); + $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 ); + } } /** diff --git a/src/wp-admin/includes/schema.php b/src/wp-admin/includes/schema.php index 1104b99871..d9e9d468a0 100644 --- a/src/wp-admin/includes/schema.php +++ b/src/wp-admin/includes/schema.php @@ -534,6 +534,7 @@ function populate_options( array $options = array() ) { // 5.5.0 'blocklist_keys' => '', 'comment_previously_approved' => 1, + 'auto_plugin_theme_update_emails' => array(), ); // 3.3.0 @@ -552,7 +553,13 @@ function populate_options( array $options = array() ) { $options = wp_parse_args( $options, $defaults ); // Set autoload to no for these options. - $fat_options = array( 'moderation_keys', 'recently_edited', 'blocklist_keys', 'uninstall_plugins' ); + $fat_options = array( + 'moderation_keys', + 'recently_edited', + 'blocklist_keys', + 'uninstall_plugins', + 'auto_plugin_theme_update_emails', + ); $keys = "'" . implode( "', '", array_keys( $options ) ) . "'"; $existing_options = $wpdb->get_col( "SELECT option_name FROM $wpdb->options WHERE option_name in ( $keys )" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared diff --git a/src/wp-includes/version.php b/src/wp-includes/version.php index 0166399578..c514aeb9a9 100644 --- a/src/wp-includes/version.php +++ b/src/wp-includes/version.php @@ -20,7 +20,7 @@ $wp_version = '5.5-alpha-47426-src'; * * @global int $wp_db_version */ -$wp_db_version = 48121; +$wp_db_version = 48397; /** * Holds the TinyMCE version.