Commit d2dfbf8d28 for wordpress.org

commit d2dfbf8d28e2ad04c41b731ee3fa5e1acfe2bdc4
Author: westonruter <westonruter@git.wordpress.org>
Date:   Mon Oct 19 20:55:51 2026 +0000

    Upgrade/Install: Report fatal errors in auto-update emails.

    When a plugin auto-update was rolled back because the loopback request found a fatal error, the failure email gave no reason, even though `WP_Automatic_Updater::has_fatal_error()` had already received the error's message, file, and line in the scrape result. These details are now added to the rollback `WP_Error` under a new `plugin_update_fatal_error` code, so the email lists them under the plugin whose update failed. This works regardless of `WP_DEBUG`, and when the loopback request itself fails, its error message is reported instead, so a rollback is no longer left unexplained.

    The details are carried on the plugin's update result rather than in a transient, so that they cannot expire during a long update run, go stale when no email is sent, or be attributed to the wrong plugin when several fail in the same run. This also leaves the signature of the protected `has_fatal_error()` method unchanged for subclasses. Only the first line of the error message is included, since the message for an uncaught exception continues with a stack trace whose argument values, such as database credentials, would otherwise leave the server by email.

    The parameters of the `send_plugin_theme_email()` and `after_plugin_theme_update()` methods are also typed with the update result types, so that PHPStan can check the new email code, which resolves a baselined error for a possibly undefined `$subject`.

    Developed in https://github.com/WordPress/wordpress-develop/pull/10445.
    Follow-up to r58128, r64435.

    Props tlloancy, westonruter, sajib1223, afragen, knutsp, im3dabasia1.
    See #65817.
    Fixes #64155.

    Built from https://develop.svn.wordpress.org/trunk@64582


    git-svn-id: http://core.svn.wordpress.org/trunk@63732 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-admin/includes/class-wp-automatic-updater.php b/wp-admin/includes/class-wp-automatic-updater.php
index 0134a78429..0bb8937d0f 100644
--- a/wp-admin/includes/class-wp-automatic-updater.php
+++ b/wp-admin/includes/class-wp-automatic-updater.php
@@ -86,6 +86,16 @@ class WP_Automatic_Updater {
 	 */
 	protected $update_results = array();

+	/**
+	 * Details of the fatal error detected by the last call to has_fatal_error().
+	 *
+	 * This is the fatal error, or the reason the loopback request to check for one failed.
+	 * It is null when no fatal error was detected.
+	 *
+	 * @since 7.2.0
+	 */
+	private ?string $fatal_error_message = null;
+
 	/**
 	 * Determines whether the entire automatic updater is disabled.
 	 *
@@ -710,6 +720,10 @@ class WP_Automatic_Updater {
 						}
 					}

+					if ( null !== $this->fatal_error_message ) {
+						$upgrade_result->add( 'plugin_update_fatal_error', $this->fatal_error_message );
+					}
+
 					/*
 					 * Should emails not be working, log the message(s) so that
 					 * the log file contains context for the fatal error,
@@ -1268,6 +1282,13 @@ class WP_Automatic_Updater {
 	 * @since 5.5.0
 	 *
 	 * @param array $update_results The results of update tasks.
+	 *
+	 * @phpstan-param array{
+	 *     core?: list<Core_Update_Result>,
+	 *     plugin?: list<Plugin_Update_Result>,
+	 *     theme?: list<Theme_Update_Result>,
+	 *     translation?: list<Translation_Update_Result>,
+	 * } $update_results
 	 */
 	protected function after_plugin_theme_update( $update_results ) {
 		$successful_updates = array();
@@ -1340,6 +1361,10 @@ class WP_Automatic_Updater {
 	 * @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.
+	 *
+	 * @phpstan-param 'success'|'fail'|'mixed' $type
+	 * @phpstan-param array{ plugin?: list<Plugin_Update_Result>, theme?: list<Theme_Update_Result> } $successful_updates
+	 * @phpstan-param array{ plugin?: list<Plugin_Update_Result>, theme?: list<Theme_Update_Result> } $failed_updates
 	 */
 	protected function send_plugin_theme_email( $type, $successful_updates, $failed_updates ) {
 		// No updates were attempted.
@@ -1487,6 +1512,13 @@ class WP_Automatic_Updater {

 					$body[] = $body_message;

+					if ( isset( $item->result ) && is_wp_error( $item->result ) ) {
+						$fatal_error = $item->result->get_error_message( 'plugin_update_fatal_error' );
+						if ( '' !== $fatal_error ) {
+							$body[] = '  ' . $fatal_error;
+						}
+					}
+
 					$past_failure_emails[ $item->item->plugin ] = $item->item->new_version;
 				}

@@ -1862,6 +1894,7 @@ Thanks! -- The WordPress Team"
 	 * Fatal errors cannot be detected unless maintenance mode is enabled.
 	 *
 	 * @since 6.6.0
+	 * @since 7.2.0 Details of a detected fatal error are recorded for the update result.
 	 *
 	 * @global int $upgrading The Unix timestamp marking when upgrading WordPress began.
 	 *
@@ -1870,6 +1903,8 @@ Thanks! -- The WordPress Team"
 	protected function has_fatal_error() {
 		global $upgrading;

+		$this->fatal_error_message = null;
+
 		$maintenance_file = ABSPATH . '.maintenance';
 		if ( ! file_exists( $maintenance_file ) ) {
 			return false;
@@ -1916,6 +1951,12 @@ Thanks! -- The WordPress Team"
 		$response  = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout', 'sslverify' ) );

 		if ( is_wp_error( $response ) ) {
+			$this->fatal_error_message = sprintf(
+				/* translators: %s: Error message. */
+				__( 'The loopback request to check for fatal errors failed: %s' ),
+				$response->get_error_message()
+			);
+
 			if ( $is_debug ) {
 				error_log( 'Loopback request failed: ' . $response->get_error_message() );
 			}
@@ -1937,6 +1978,19 @@ Thanks! -- The WordPress Team"
 			$result       = json_decode( trim( $error_output ), true );
 		}

+		if ( is_array( $result ) && isset( $result['type'] ) && ! empty( $result['message'] ) && is_string( $result['message'] ) ) {
+			/*
+			 * Only the first line of the message is kept, as the message for an uncaught exception
+			 * continues with a stack trace, which can include the values of function arguments.
+			 */
+			$this->fatal_error_message = sprintf(
+				'PHP Fatal error: %s in %s on line %d',
+				explode( "\n", $result['message'], 2 )[0],
+				isset( $result['file'] ) && is_string( $result['file'] ) ? $result['file'] : 'unknown',
+				isset( $result['line'] ) && is_numeric( $result['line'] ) ? (int) $result['line'] : 0
+			);
+		}
+
 		delete_transient( $transient );

 		// Only fatal errors will result in a 'type' key.
diff --git a/wp-includes/version.php b/wp-includes/version.php
index b0d02e31ab..afc85a920d 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-alpha-64581';
+$wp_version = '7.2-alpha-64582';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.