From 38006478cea7fc06d52f954446db345229a79ebd Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sat, 1 Nov 2025 00:37:42 +0100 Subject: [PATCH 01/17] Administration: Add last fatal PHP error to auto-update failure emails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admins must know *why* an update failed — especially when rollback hides the crash. ### Changes - Captures fatal error in `has_fatal_error()` via `error_get_last()` or `debug.log` - Stores in global transient `wp_last_fatal_error` - Adds to failure email with clear English message - No performance impact — only runs on failure - Fully backward compatible ### Testing - Tested on WP 6.8.3 with memory exhaustion (WooCommerce) - Email includes exact error: `PHP Fatal error: Allowed memory size... in class-wc-autoloader.php on line 58` FIXES #64155 --- .../includes/class-wp-automatic-updater.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index aa4db87effc2a..e88910546ea7b 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1540,6 +1540,17 @@ protected function send_plugin_theme_email( $type, $successful_updates, $failed_ */ $email = apply_filters( 'auto_plugin_theme_update_email', $email, $type, $successful_updates, $failed_updates ); + if ( defined( 'WP_DEBUG' ) && WP_DEBUG && $type === 'fail' ) { + $transient_key = 'wp_last_fatal_error'; + $fatal_error = get_transient( $transient_key ); + if ( $fatal_error ) { + $email['body'] .= "\n\n=== LAST FATAL ERROR (PHP) ===\n"; + $email['body'] .= "• " . $fatal_error . "\n"; + $email['body'] .= "========================================\n"; + delete_transient( $transient_key ); + } + } + $result = wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] ); if ( $result ) { @@ -1828,6 +1839,27 @@ protected function has_fatal_error() { $result = json_decode( trim( $error_output ), true ); } + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + $fatal_error = null; + + $last_error = error_get_last(); + if ( $last_error && in_array( $last_error['type'], [1,2,4,256] ) ) { + $fatal_error = "PHP Fatal error: {$last_error['message']} in {$last_error['file']} on line {$last_error['line']}"; + } elseif ( file_exists( WP_CONTENT_DIR . '/debug.log' ) ) { + $lines = array_reverse( file( WP_CONTENT_DIR . '/debug.log', FILE_IGNORE_NEW_LINES ) ); + foreach ( $lines as $line ) { + if ( strpos( $line, 'PHP Fatal error' ) !== false ) { + $fatal_error .= trim( $line ); + break; + } + } + } + + if ( $fatal_error ) { + set_transient( 'wp_last_fatal_error', $fatal_error, 300 ); + } + } + delete_transient( $transient ); // Only fatal errors will result in a 'type' key. From 9b53ee56dc047853f12e7b562663efd7d83465a8 Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sat, 1 Nov 2025 00:44:03 +0100 Subject: [PATCH 02/17] Fix: Use = instead of .= to avoid "null" prefix --- src/wp-admin/includes/class-wp-automatic-updater.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index e88910546ea7b..61268b46bbc5d 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1849,7 +1849,7 @@ protected function has_fatal_error() { $lines = array_reverse( file( WP_CONTENT_DIR . '/debug.log', FILE_IGNORE_NEW_LINES ) ); foreach ( $lines as $line ) { if ( strpos( $line, 'PHP Fatal error' ) !== false ) { - $fatal_error .= trim( $line ); + $fatal_error = trim( $line ); break; } } From d3e0235367664ece91612f970bbbd0b779887a0e Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sat, 1 Nov 2025 01:06:13 +0100 Subject: [PATCH 03/17] Fix: Read debug.log safely from end, avoid memory crash on large files - Replace `file()` + `array_reverse()` with backward line-by-line reading - Safe for BIG debug.log - Use `E_*` constants for clarity Fixes #64155 --- .../includes/class-wp-automatic-updater.php | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index 61268b46bbc5d..d3da10acdca18 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1843,14 +1843,39 @@ protected function has_fatal_error() { $fatal_error = null; $last_error = error_get_last(); - if ( $last_error && in_array( $last_error['type'], [1,2,4,256] ) ) { - $fatal_error = "PHP Fatal error: {$last_error['message']} in {$last_error['file']} on line {$last_error['line']}"; - } elseif ( file_exists( WP_CONTENT_DIR . '/debug.log' ) ) { - $lines = array_reverse( file( WP_CONTENT_DIR . '/debug.log', FILE_IGNORE_NEW_LINES ) ); - foreach ( $lines as $line ) { - if ( strpos( $line, 'PHP Fatal error' ) !== false ) { - $fatal_error = trim( $line ); - break; + if ( $last_error && in_array( $last_error['type'], [ E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR ] ) ) { + $fatal_error = "PHP Fatal error: {$last_error['message']} in {$last_error['file']} on line {$last_error['line']}"; + } else { + $log_file = WP_CONTENT_DIR . '/debug.log'; + if ( file_exists( $log_file ) && is_readable( $log_file ) ) { + $handle = fopen( $log_file, 'r' ); + if ( $handle ) { + $pos = -2; + $current_line = ''; + while ( $pos > -filesize( $log_file ) ) { + fseek( $handle, $pos, SEEK_END ); + $char = fgetc( $handle ); + if ( $char === "\n" ) { + if ( $current_line !== '' ) { + $line = strrev( $current_line ); + if ( strpos( $line, 'PHP Fatal error' ) !== false ) { + $fatal_error = trim( $line ); + break; + } + $current_line = ''; + } + } else { + $current_line .= $char; + } + $pos--; + } + if ( $current_line !== '' && $fatal_error === null ) { + $line = strrev( $current_line ); + if ( strpos( $line, 'PHP Fatal error' ) !== false ) { + $fatal_error = trim( $line ); + } + } + fclose( $handle ); } } } From 491b07e9eb546ca9161710a84f49463794e46119 Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sat, 1 Nov 2025 02:03:06 +0100 Subject: [PATCH 04/17] Fix: Apply WordPress coding standards (PHPCS) - Use Yoda conditions - Use single quotes for simple strings - Use array() instead of [] - Remove trailing whitespace - Use E_* constants --- .../includes/class-wp-automatic-updater.php | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index d3da10acdca18..03288701d5c1f 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1540,17 +1540,16 @@ protected function send_plugin_theme_email( $type, $successful_updates, $failed_ */ $email = apply_filters( 'auto_plugin_theme_update_email', $email, $type, $successful_updates, $failed_updates ); - if ( defined( 'WP_DEBUG' ) && WP_DEBUG && $type === 'fail' ) { - $transient_key = 'wp_last_fatal_error'; - $fatal_error = get_transient( $transient_key ); + if ( defined( 'WP_DEBUG' ) && WP_DEBUG && 'fail' === $type ) { + $fatal_error = get_transient( 'wp_last_fatal_error' ); if ( $fatal_error ) { - $email['body'] .= "\n\n=== LAST FATAL ERROR (PHP) ===\n"; - $email['body'] .= "• " . $fatal_error . "\n"; + $email['body'] .= "\n\n=== LAST FATAL PHP ERROR ===\n"; + $email['body'] .= '• ' . $fatal_error . "\n"; $email['body'] .= "========================================\n"; - delete_transient( $transient_key ); + delete_transient( 'wp_last_fatal_error' ); } } - + $result = wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] ); if ( $result ) { @@ -1843,8 +1842,8 @@ protected function has_fatal_error() { $fatal_error = null; $last_error = error_get_last(); - if ( $last_error && in_array( $last_error['type'], [ E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR ] ) ) { - $fatal_error = "PHP Fatal error: {$last_error['message']} in {$last_error['file']} on line {$last_error['line']}"; + if ( $last_error && in_array( $last_error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR ) ) ) { + $fatal_error = "PHP Fatal error: {$last_error['message']} in {$last_error['file']} on line {$last_error['line']}"; } else { $log_file = WP_CONTENT_DIR . '/debug.log'; if ( file_exists( $log_file ) && is_readable( $log_file ) ) { @@ -1855,10 +1854,10 @@ protected function has_fatal_error() { while ( $pos > -filesize( $log_file ) ) { fseek( $handle, $pos, SEEK_END ); $char = fgetc( $handle ); - if ( $char === "\n" ) { - if ( $current_line !== '' ) { + if ( "\n" === $char ) { + if ( '' !== $current_line ) { $line = strrev( $current_line ); - if ( strpos( $line, 'PHP Fatal error' ) !== false ) { + if ( false !== strpos( $line, 'PHP Fatal error' ) ) { $fatal_error = trim( $line ); break; } @@ -1869,9 +1868,9 @@ protected function has_fatal_error() { } $pos--; } - if ( $current_line !== '' && $fatal_error === null ) { + if ( '' !== $current_line && null === $fatal_error ) { $line = strrev( $current_line ); - if ( strpos( $line, 'PHP Fatal error' ) !== false ) { + if ( false !== strpos( $line, 'PHP Fatal error' ) ) { $fatal_error = trim( $line ); } } @@ -1881,10 +1880,10 @@ protected function has_fatal_error() { } if ( $fatal_error ) { - set_transient( 'wp_last_fatal_error', $fatal_error, 300 ); + set_transient( 'wp_last_fatal_error', $fatal_error, 5 * MINUTE_IN_SECONDS ); } } - + delete_transient( $transient ); // Only fatal errors will result in a 'type' key. From 930bbfde7f96e6794b791d4d841977c684d2cf6b Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sat, 1 Nov 2025 03:24:21 +0100 Subject: [PATCH 05/17] Final: Full compliance with core review - Use ini_get('error_log') with safe fallback - Specific transient: wp_updater_last_fatal_error - Remove defined('WP_DEBUG') - Translate header only - 0 RAM, tested on 400MB+ logs Props: @westonruter, @knutsp --- .../includes/class-wp-automatic-updater.php | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index 03288701d5c1f..0c0c0a8114b85 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1540,13 +1540,13 @@ protected function send_plugin_theme_email( $type, $successful_updates, $failed_ */ $email = apply_filters( 'auto_plugin_theme_update_email', $email, $type, $successful_updates, $failed_updates ); - if ( defined( 'WP_DEBUG' ) && WP_DEBUG && 'fail' === $type ) { - $fatal_error = get_transient( 'wp_last_fatal_error' ); + if ( WP_DEBUG && 'fail' === $type ) { + $fatal_error = get_transient( 'wp_updater_last_fatal_error' ); if ( $fatal_error ) { - $email['body'] .= "\n\n=== LAST FATAL PHP ERROR ===\n"; + $email['body'] .= "\n\n=== " . __( 'LAST FATAL PHP ERROR' ) . " ===\n"; $email['body'] .= '• ' . $fatal_error . "\n"; $email['body'] .= "========================================\n"; - delete_transient( 'wp_last_fatal_error' ); + delete_transient( 'wp_updater_last_fatal_error' ); } } @@ -1838,15 +1838,21 @@ protected function has_fatal_error() { $result = json_decode( trim( $error_output ), true ); } - if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + if ( WP_DEBUG ) { $fatal_error = null; $last_error = error_get_last(); if ( $last_error && in_array( $last_error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR ) ) ) { $fatal_error = "PHP Fatal error: {$last_error['message']} in {$last_error['file']} on line {$last_error['line']}"; } else { - $log_file = WP_CONTENT_DIR . '/debug.log'; - if ( file_exists( $log_file ) && is_readable( $log_file ) ) { + $log_file = ini_get( 'error_log' ); + + if ( ! $log_file ) { + error_log( 'WP Auto-Update: error_log is empty, falling back to debug.log' ); + $log_file = WP_CONTENT_DIR . '/debug.log'; + } + + if ( $log_file && file_exists( $log_file ) && is_readable( $log_file ) ) { $handle = fopen( $log_file, 'r' ); if ( $handle ) { $pos = -2; @@ -1880,7 +1886,7 @@ protected function has_fatal_error() { } if ( $fatal_error ) { - set_transient( 'wp_last_fatal_error', $fatal_error, 5 * MINUTE_IN_SECONDS ); + set_transient( 'wp_updater_last_fatal_error', $fatal_error, 5 * MINUTE_IN_SECONDS ); } } From 1a4002c4fba88af13ff66d765fbdb734f1f7040b Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sun, 9 Nov 2025 14:05:47 +0100 Subject: [PATCH 06/17] Update class-wp-automatic-updater.php - mixed state also a fail from what i saw state mixed state is also part of the failures so it to --- src/wp-admin/includes/class-wp-automatic-updater.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index 0c0c0a8114b85..8cf8d2f6e7923 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1540,7 +1540,7 @@ protected function send_plugin_theme_email( $type, $successful_updates, $failed_ */ $email = apply_filters( 'auto_plugin_theme_update_email', $email, $type, $successful_updates, $failed_updates ); - if ( WP_DEBUG && 'fail' === $type ) { + if ( WP_DEBUG && ( 'fail' === $type || 'mixed' === $type ) ) { $fatal_error = get_transient( 'wp_updater_last_fatal_error' ); if ( $fatal_error ) { $email['body'] .= "\n\n=== " . __( 'LAST FATAL PHP ERROR' ) . " ===\n"; From 7a2c909786ee4e6da108224469b6a26632333a64 Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sun, 9 Nov 2025 14:13:18 +0100 Subject: [PATCH 07/17] Update class-wp-automatic-updater.php extra space --- src/wp-admin/includes/class-wp-automatic-updater.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index 8cf8d2f6e7923..095e78543f80a 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1540,7 +1540,7 @@ protected function send_plugin_theme_email( $type, $successful_updates, $failed_ */ $email = apply_filters( 'auto_plugin_theme_update_email', $email, $type, $successful_updates, $failed_updates ); - if ( WP_DEBUG && ( 'fail' === $type || 'mixed' === $type ) ) { + if ( WP_DEBUG && ( 'fail' === $type || 'mixed' === $type ) ) { $fatal_error = get_transient( 'wp_updater_last_fatal_error' ); if ( $fatal_error ) { $email['body'] .= "\n\n=== " . __( 'LAST FATAL PHP ERROR' ) . " ===\n"; From 7f9c1ea559404b95d68e619bda7226333b633ef7 Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sun, 9 Nov 2025 14:18:10 +0100 Subject: [PATCH 08/17] Update class-wp-automatic-updater.php extra space again --- src/wp-admin/includes/class-wp-automatic-updater.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index 095e78543f80a..4c63efd58383f 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1540,7 +1540,7 @@ protected function send_plugin_theme_email( $type, $successful_updates, $failed_ */ $email = apply_filters( 'auto_plugin_theme_update_email', $email, $type, $successful_updates, $failed_updates ); - if ( WP_DEBUG && ( 'fail' === $type || 'mixed' === $type ) ) { + if ( WP_DEBUG && ( 'fail' === $type || 'mixed' === $type ) ) { $fatal_error = get_transient( 'wp_updater_last_fatal_error' ); if ( $fatal_error ) { $email['body'] .= "\n\n=== " . __( 'LAST FATAL PHP ERROR' ) . " ===\n"; From 00e89af8ab71ab86ae0c18cc47f1771e98ca05c1 Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sat, 28 Mar 2026 21:02:22 +0100 Subject: [PATCH 09/17] Update class-wp-automatic-updater.php ==>>> Translate section header --- src/wp-admin/includes/class-wp-automatic-updater.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index 4c63efd58383f..be8617c5e03bd 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1543,7 +1543,7 @@ protected function send_plugin_theme_email( $type, $successful_updates, $failed_ if ( WP_DEBUG && ( 'fail' === $type || 'mixed' === $type ) ) { $fatal_error = get_transient( 'wp_updater_last_fatal_error' ); if ( $fatal_error ) { - $email['body'] .= "\n\n=== " . __( 'LAST FATAL PHP ERROR' ) . " ===\n"; + $email['body'] .= "\n\n=== " . __( 'Last fatal PHP error', 'default' ) . " ===\n"; $email['body'] .= '• ' . $fatal_error . "\n"; $email['body'] .= "========================================\n"; delete_transient( 'wp_updater_last_fatal_error' ); From c0e6616f3b9a8aa8844a3e23d25fa272cdc65880 Mon Sep 17 00:00:00 2001 From: tlloancy Date: Fri, 29 May 2026 00:16:06 +0200 Subject: [PATCH 10/17] Update src/wp-admin/includes/class-wp-automatic-updater.php Co-authored-by: Weston Ruter --- src/wp-admin/includes/class-wp-automatic-updater.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index be8617c5e03bd..e31dd5e5f24b7 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1872,7 +1872,7 @@ protected function has_fatal_error() { } else { $current_line .= $char; } - $pos--; + --$pos; } if ( '' !== $current_line && null === $fatal_error ) { $line = strrev( $current_line ); From 50eaf0648d90749cac91bfe7b23091ea7e152aaf Mon Sep 17 00:00:00 2001 From: tlloancy Date: Fri, 29 May 2026 00:17:32 +0200 Subject: [PATCH 11/17] Update src/wp-admin/includes/class-wp-automatic-updater.php Co-authored-by: Weston Ruter --- src/wp-admin/includes/class-wp-automatic-updater.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index e31dd5e5f24b7..2c39a215fd0cf 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1852,7 +1852,7 @@ protected function has_fatal_error() { $log_file = WP_CONTENT_DIR . '/debug.log'; } - if ( $log_file && file_exists( $log_file ) && is_readable( $log_file ) ) { + if ( file_exists( $log_file ) && is_readable( $log_file ) ) { $handle = fopen( $log_file, 'r' ); if ( $handle ) { $pos = -2; From d6e08cfe641afaf9decae1b9144a35e86776851b Mon Sep 17 00:00:00 2001 From: tlloancy Date: Fri, 29 May 2026 00:19:15 +0200 Subject: [PATCH 12/17] Update src/wp-admin/includes/class-wp-automatic-updater.php Co-authored-by: Weston Ruter --- src/wp-admin/includes/class-wp-automatic-updater.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index 2c39a215fd0cf..246049c21d6d4 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1542,7 +1542,7 @@ protected function send_plugin_theme_email( $type, $successful_updates, $failed_ if ( WP_DEBUG && ( 'fail' === $type || 'mixed' === $type ) ) { $fatal_error = get_transient( 'wp_updater_last_fatal_error' ); - if ( $fatal_error ) { + if ( is_string( $fatal_error ) ) { $email['body'] .= "\n\n=== " . __( 'Last fatal PHP error', 'default' ) . " ===\n"; $email['body'] .= '• ' . $fatal_error . "\n"; $email['body'] .= "========================================\n"; From dfc3ee660ca42c1a46482b86ed24e5d72190abae Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sun, 23 Aug 2026 16:33:59 +0200 Subject: [PATCH 13/17] Update class-wp-automatic-updater.php Fix plugin update error details capture without WP_DEBUG dependency - Extract fatal error directly from loopback scrape result ($result) instead of reading error logs or using error_get_last(). - Remove WP_DEBUG condition check in auto-update notification email to work in production environments. --- .../includes/class-wp-automatic-updater.php | 59 +++---------------- 1 file changed, 9 insertions(+), 50 deletions(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index 246049c21d6d4..3438ae9b45a9d 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1540,7 +1540,7 @@ protected function send_plugin_theme_email( $type, $successful_updates, $failed_ */ $email = apply_filters( 'auto_plugin_theme_update_email', $email, $type, $successful_updates, $failed_updates ); - if ( WP_DEBUG && ( 'fail' === $type || 'mixed' === $type ) ) { + if ( 'fail' === $type || 'mixed' === $type ) { $fatal_error = get_transient( 'wp_updater_last_fatal_error' ); if ( is_string( $fatal_error ) ) { $email['body'] .= "\n\n=== " . __( 'Last fatal PHP error', 'default' ) . " ===\n"; @@ -1838,56 +1838,15 @@ protected function has_fatal_error() { $result = json_decode( trim( $error_output ), true ); } - if ( WP_DEBUG ) { - $fatal_error = null; - - $last_error = error_get_last(); - if ( $last_error && in_array( $last_error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR ) ) ) { - $fatal_error = "PHP Fatal error: {$last_error['message']} in {$last_error['file']} on line {$last_error['line']}"; - } else { - $log_file = ini_get( 'error_log' ); - - if ( ! $log_file ) { - error_log( 'WP Auto-Update: error_log is empty, falling back to debug.log' ); - $log_file = WP_CONTENT_DIR . '/debug.log'; - } - - if ( file_exists( $log_file ) && is_readable( $log_file ) ) { - $handle = fopen( $log_file, 'r' ); - if ( $handle ) { - $pos = -2; - $current_line = ''; - while ( $pos > -filesize( $log_file ) ) { - fseek( $handle, $pos, SEEK_END ); - $char = fgetc( $handle ); - if ( "\n" === $char ) { - if ( '' !== $current_line ) { - $line = strrev( $current_line ); - if ( false !== strpos( $line, 'PHP Fatal error' ) ) { - $fatal_error = trim( $line ); - break; - } - $current_line = ''; - } - } else { - $current_line .= $char; - } - --$pos; - } - if ( '' !== $current_line && null === $fatal_error ) { - $line = strrev( $current_line ); - if ( false !== strpos( $line, 'PHP Fatal error' ) ) { - $fatal_error = trim( $line ); - } - } - fclose( $handle ); - } - } - } + if ( is_array( $result ) && ! empty( $result['message'] ) ) { + $fatal_error = sprintf( + 'PHP Fatal error: %s in %s on line %d', + $result['message'], + $result['file'] ?? 'unknown', + $result['line'] ?? 0 + ); - if ( $fatal_error ) { - set_transient( 'wp_updater_last_fatal_error', $fatal_error, 5 * MINUTE_IN_SECONDS ); - } + set_transient( 'wp_updater_last_fatal_error', $fatal_error, 5 * MINUTE_IN_SECONDS ); } delete_transient( $transient ); From 0ac14ebe7312d629439670c6f74637110074cc35 Mon Sep 17 00:00:00 2001 From: tlloancy Date: Wed, 2 Sep 2026 22:20:14 +0200 Subject: [PATCH 14/17] Update src/wp-admin/includes/class-wp-automatic-updater.php Co-authored-by: Weston Ruter --- src/wp-admin/includes/class-wp-automatic-updater.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index 3438ae9b45a9d..1ac360a02eead 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1838,7 +1838,7 @@ protected function has_fatal_error() { $result = json_decode( trim( $error_output ), true ); } - if ( is_array( $result ) && ! empty( $result['message'] ) ) { + if ( is_array( $result ) && ! empty( $result['message'] ) && is_string( $result['message'] ) ) { $fatal_error = sprintf( 'PHP Fatal error: %s in %s on line %d', $result['message'], From cc543becdff90eb9d468a8f751d20b3cc680dd6c Mon Sep 17 00:00:00 2001 From: tlloancy Date: Fri, 11 Sep 2026 22:33:25 +0200 Subject: [PATCH 15/17] Update class-wp-automatic-updater.php Administration: Store fatal errors per-plugin, expose to email filter Fixes two points from the latest testing report on #64155: - has_fatal_error() now takes $plugin_slug and stores errors in an array keyed by slug, so multiple simultaneous plugin fatals are all preserved and listed individually in the email (previously only the last one survived). - The error section is now built before apply_filters() runs, so auto_plugin_theme_update_email can see and modify it. Also added missing type checks on $result['file']/$result['line']. Props: sajib1223 Fixes #64155 --- .../includes/class-wp-automatic-updater.php | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index 1ac360a02eead..b8c4d2bc32eaa 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -563,7 +563,7 @@ public function update( $type, $item ) { */ sleep( 2 ); - if ( $this->has_fatal_error() ) { + if ( $this->has_fatal_error( $item->slug ) ) { $upgrade_result = new WP_Error(); $temp_backup = array( array( @@ -1538,17 +1538,22 @@ protected function send_plugin_theme_email( $type, $successful_updates, $failed_ * @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 ); - if ( 'fail' === $type || 'mixed' === $type ) { - $fatal_error = get_transient( 'wp_updater_last_fatal_error' ); - if ( is_string( $fatal_error ) ) { + $fatal_errors = get_transient( 'wp_updater_last_fatal_error' ); + if ( is_array( $fatal_errors ) && ! empty( $fatal_errors ) ) { $email['body'] .= "\n\n=== " . __( 'Last fatal PHP error', 'default' ) . " ===\n"; - $email['body'] .= '• ' . $fatal_error . "\n"; + foreach ( $fatal_errors as $plugin_slug => $fatal_error ) { + if ( ! is_string( $fatal_error ) || ! is_string( $plugin_slug ) ) { + continue; + } + $email['body'] .= '• [' . $plugin_slug . '] ' . $fatal_error . "\n"; + } $email['body'] .= "========================================\n"; delete_transient( 'wp_updater_last_fatal_error' ); } } + + $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'] ); @@ -1763,12 +1768,15 @@ protected function send_debug_email() { * Fatal errors cannot be detected unless maintenance mode is enabled. * * @since 6.6.0 + * @since 7.2.0 Added the $plugin_slug parameter to support tracking errors per plugin. * * @global int $upgrading The Unix timestamp marking when upgrading WordPress began. * + * @param string $plugin_slug Optional. The slug of the plugin being checked, used to + * key the stored fatal error by plugin. Default empty string. * @return bool Whether a fatal error was detected. */ - protected function has_fatal_error() { + protected function has_fatal_error( $plugin_slug = '' ) { global $upgrading; $maintenance_file = ABSPATH . '.maintenance'; @@ -1842,11 +1850,19 @@ protected function has_fatal_error() { $fatal_error = sprintf( 'PHP Fatal error: %s in %s on line %d', $result['message'], - $result['file'] ?? 'unknown', - $result['line'] ?? 0 + is_string( $result['file'] ?? null ) ? $result['file'] : 'unknown', + is_int( $result['line'] ?? null ) ? $result['line'] : 0 ); - set_transient( 'wp_updater_last_fatal_error', $fatal_error, 5 * MINUTE_IN_SECONDS ); + $errors = get_transient( 'wp_updater_last_fatal_error' ); + if ( ! is_array( $errors ) ) { + $errors = array(); + } + + $slug_key = is_string( $plugin_slug ) && '' !== $plugin_slug ? $plugin_slug : 'unknown'; + $errors[ $slug_key ] = $fatal_error; + + set_transient( 'wp_updater_last_fatal_error', $errors, 5 * MINUTE_IN_SECONDS ); } delete_transient( $transient ); From 00b1980f11fcbc3e1ca2a180ad2fc0d0c7aff6be Mon Sep 17 00:00:00 2001 From: tlloancy Date: Fri, 11 Sep 2026 22:46:28 +0200 Subject: [PATCH 16/17] Fix: Remove trailing whitespace and revert is_string()/is_int() validation Unnecessary since $result['message'] is already validated right above --- src/wp-admin/includes/class-wp-automatic-updater.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index b8c4d2bc32eaa..aff14af491f89 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1552,7 +1552,7 @@ protected function send_plugin_theme_email( $type, $successful_updates, $failed_ delete_transient( 'wp_updater_last_fatal_error' ); } } - + $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'] ); @@ -1850,8 +1850,8 @@ protected function has_fatal_error( $plugin_slug = '' ) { $fatal_error = sprintf( 'PHP Fatal error: %s in %s on line %d', $result['message'], - is_string( $result['file'] ?? null ) ? $result['file'] : 'unknown', - is_int( $result['line'] ?? null ) ? $result['line'] : 0 + $result['file'] ?? 'unknown', + $result['line'] ?? 0 ); $errors = get_transient( 'wp_updater_last_fatal_error' ); From 43b58aa5ae1449cfbdd8cffefdc0d8656834ab34 Mon Sep 17 00:00:00 2001 From: tlloancy Date: Fri, 11 Sep 2026 23:46:31 +0200 Subject: [PATCH 17/17] Fix: Move fatal error block above filter docblock --- .../includes/class-wp-automatic-updater.php | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/wp-admin/includes/class-wp-automatic-updater.php b/src/wp-admin/includes/class-wp-automatic-updater.php index aff14af491f89..755e99feec5ea 100644 --- a/src/wp-admin/includes/class-wp-automatic-updater.php +++ b/src/wp-admin/includes/class-wp-automatic-updater.php @@ -1520,6 +1520,21 @@ protected function send_plugin_theme_email( $type, $successful_updates, $failed_ $email = compact( 'to', 'subject', 'body', 'headers' ); + if ( 'fail' === $type || 'mixed' === $type ) { + $fatal_errors = get_transient( 'wp_updater_last_fatal_error' ); + if ( is_array( $fatal_errors ) && ! empty( $fatal_errors ) ) { + $email['body'] .= "\n\n=== " . __( 'Last fatal PHP error', 'default' ) . " ===\n"; + foreach ( $fatal_errors as $plugin_slug => $fatal_error ) { + if ( ! is_string( $fatal_error ) || ! is_string( $plugin_slug ) ) { + continue; + } + $email['body'] .= '• [' . $plugin_slug . '] ' . $fatal_error . "\n"; + } + $email['body'] .= "========================================\n"; + delete_transient( 'wp_updater_last_fatal_error' ); + } + } + /** * Filters the email sent following an automatic background update for plugins and themes. * @@ -1538,21 +1553,6 @@ protected function send_plugin_theme_email( $type, $successful_updates, $failed_ * @param array $successful_updates A list of updates that succeeded. * @param array $failed_updates A list of updates that failed. */ - if ( 'fail' === $type || 'mixed' === $type ) { - $fatal_errors = get_transient( 'wp_updater_last_fatal_error' ); - if ( is_array( $fatal_errors ) && ! empty( $fatal_errors ) ) { - $email['body'] .= "\n\n=== " . __( 'Last fatal PHP error', 'default' ) . " ===\n"; - foreach ( $fatal_errors as $plugin_slug => $fatal_error ) { - if ( ! is_string( $fatal_error ) || ! is_string( $plugin_slug ) ) { - continue; - } - $email['body'] .= '• [' . $plugin_slug . '] ' . $fatal_error . "\n"; - } - $email['body'] .= "========================================\n"; - delete_transient( 'wp_updater_last_fatal_error' ); - } - } - $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'] );