From a5e78c5a63e320ff4153a52e70b8539b09e49fad Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sat, 1 Nov 2025 00:37:42 +0100 Subject: [PATCH 1/8] 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 2facbeb1d522f..797fa2b7990d6 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 755902ccaf5ca4002afc94028dc864d4ddf79108 Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sat, 1 Nov 2025 00:44:03 +0100 Subject: [PATCH 2/8] 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 797fa2b7990d6..990fe9bfebf64 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 c36f172857892b19e86ce257fb43bf806cf1141d Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sat, 1 Nov 2025 01:06:13 +0100 Subject: [PATCH 3/8] 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 990fe9bfebf64..7900a773a1198 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 e2cd6fdc0523812a8ef797c3a3a4262ae27f3e88 Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sat, 1 Nov 2025 02:03:06 +0100 Subject: [PATCH 4/8] 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 7900a773a1198..1198ae5d2006d 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 c63ae8c1336d6c32708588643c0342696961b98f Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sat, 1 Nov 2025 03:24:21 +0100 Subject: [PATCH 5/8] 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 1198ae5d2006d..557e521d448b2 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 f406f17db3c0c60b0d2c8d52ee33e5534d407427 Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sun, 9 Nov 2025 14:05:47 +0100 Subject: [PATCH 6/8] 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 557e521d448b2..46df72c8d9f44 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 2f33c4a52fe48918693a5f598e7900d6eda1dcfd Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sun, 9 Nov 2025 14:13:18 +0100 Subject: [PATCH 7/8] 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 46df72c8d9f44..6c03da6df5ca9 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 7af4b90cf6a7aaecaf7b6fe2494cf83b51812a64 Mon Sep 17 00:00:00 2001 From: tlloancy Date: Sun, 9 Nov 2025 14:18:10 +0100 Subject: [PATCH 8/8] 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 6c03da6df5ca9..f583b7571b2b8 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";