From bfb896ce08fe5356f49c3fea98c481aa90ea6d95 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Tue, 7 Jul 2020 18:58:32 +0000 Subject: [PATCH] Administration: Prevent repeat emails for identical plugin or theme auto-update attempt failures. This change adds a throttle mechanism to plugin and theme auto-update failure emails using similar logic to the email sent for a Core auto-update. The first time a plugin or theme auto-update fails, the package and `new_version` will be tracked in the `auto_plugin_theme_update_emails` option. An email for this specific update attempt will not be resent. However, if this update fails again and non-repeat failures or successful updates are also present, then the failure information will be included in that email (an email needs to be sent for the new events regardless). Props johnbillion, arpitgshah, desrosj, audrasjb, pbiron, earnjam. Fixes #50448. git-svn-id: https://develop.svn.wordpress.org/trunk@48397 602fd350-edb4-49c9-b593-d223f7449a82 --- .../includes/class-plugin-upgrader.php | 23 +++++++++++ .../includes/class-theme-upgrader.php | 23 +++++++++++ .../includes/class-wp-automatic-updater.php | 40 +++++++++++++++++-- src/wp-admin/includes/schema.php | 9 ++++- src/wp-includes/version.php | 2 +- 5 files changed, 92 insertions(+), 5 deletions(-) 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.