From fcc23c8f03730d837f9defcfae630b0fe454aeb0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 16 Oct 2025 12:34:52 -0700 Subject: [PATCH 01/22] Capture errors emitted while applying wp_template_enhancement_output_buffer filters --- src/wp-includes/template.php | 50 +++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index f799113e9c26e..a14cdd14aef48 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -947,6 +947,22 @@ function wp_finalize_template_enhancement_output_buffer( string $output, int $ph $filtered_output = $output; + // TODO: Also capture exceptions? + $error_log = array(); + $display_errors = ini_get( 'display_errors' ); + if ( $display_errors ) { + ini_set( 'display_errors', 0 ); + set_error_handler( + static function ( int $level, string $message, ?string $file = null, ?int $line = null, ?array $context = null ) use ( &$error_log ) { + if ( error_reporting() & $level ) { + $error_log[] = compact( 'level', 'message', 'file', 'line', 'context' ); + } + return false; + }, + E_ALL + ); + } + /** * Filters the template enhancement output buffer prior to sending to the client. * @@ -962,5 +978,37 @@ function wp_finalize_template_enhancement_output_buffer( string $output, int $ph * @param string $filtered_output HTML template enhancement output buffer. * @param string $output Original HTML template output buffer. */ - return (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output ); + $filtered_output = (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output ); + + if ( $display_errors ) { + foreach ( $error_log as $error ) { + switch ( $error['level'] ) { + case E_USER_NOTICE: + $type = 'Notice'; + break; + case E_USER_DEPRECATED: + $type = 'Deprecated'; + break; + case E_USER_WARNING: + $type = 'Warning'; + break; + default: + $type = 'Error'; + } + $displayed_error = sprintf( "
\n%s: %s", $type, $error['message'] ); + if ( null !== $error['file'] ) { + $displayed_error .= sprintf( ' in %s', $error['file'] ); + if ( null !== $error['line'] ) { + $displayed_error .= sprintf( ' on line %d', $error['line'] ); + } + } + $displayed_error .= '
'; + + $filtered_output .= $displayed_error; + } + restore_error_handler(); + ini_set( 'display_errors', 1 ); + } + + return $filtered_output; } From 716a9be977485ff5413fa08c30dc732146bd807f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 29 Oct 2025 17:51:24 -0700 Subject: [PATCH 02/22] Remove redundant E_ALL --- src/wp-includes/template.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 144ab5c469152..932ca54bb4b99 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -976,8 +976,7 @@ static function ( int $level, string $message, ?string $file = null, ?int $line $error_log[] = compact( 'level', 'message', 'file', 'line', 'context' ); } return false; - }, - E_ALL + } ); } From d9c167a11bf4b0630b37a59949db24f228a54544 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 29 Oct 2025 17:54:11 -0700 Subject: [PATCH 03/22] Restore error handler only after the wp_send_late_headers action has fired --- src/wp-includes/template.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 932ca54bb4b99..1bd26840a9795 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -1023,8 +1023,6 @@ static function ( int $level, string $message, ?string $file = null, ?int $line $filtered_output .= $displayed_error; } - restore_error_handler(); - ini_set( 'display_errors', 1 ); } /** @@ -1045,5 +1043,10 @@ static function ( int $level, string $message, ?string $file = null, ?int $line */ do_action( 'wp_send_late_headers', $filtered_output ); + if ( $display_errors ) { + restore_error_handler(); + ini_set( 'display_errors', 1 ); + } + return $filtered_output; } From 27d9e835dfe3f1cfccf2a6ec36846c2bd644b663 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 29 Oct 2025 22:32:30 -0700 Subject: [PATCH 04/22] Catch exceptions when firing hooks in output buffer callback --- src/wp-includes/template.php | 100 +++++++++++++++++++++++------------ 1 file changed, 66 insertions(+), 34 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 1bd26840a9795..a6e4ada74d043 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -965,7 +965,6 @@ function wp_finalize_template_enhancement_output_buffer( string $output, int $ph $filtered_output = $output; - // TODO: Also capture exceptions? $error_log = array(); $display_errors = ini_get( 'display_errors' ); if ( $display_errors ) { @@ -980,22 +979,41 @@ static function ( int $level, string $message, ?string $file = null, ?int $line ); } - /** - * Filters the template enhancement output buffer prior to sending to the client. - * - * This filter only applies the HTML output of an included template. This filter is a progressive enhancement - * intended for applications such as optimizing markup to improve frontend page load performance. Sites must not - * depend on this filter applying since they may opt to stream the responses instead. Callbacks for this filter are - * highly discouraged from using regular expressions to do any kind of replacement on the output. Use the HTML API - * (either `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use {@see DOM\HtmlDocument} as of PHP 8.4 which - * fully supports HTML5. - * - * @since 6.9.0 - * - * @param string $filtered_output HTML template enhancement output buffer. - * @param string $output Original HTML template output buffer. - */ - $filtered_output = (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output ); + try { + /** + * Filters the template enhancement output buffer prior to sending to the client. + * + * This filter only applies the HTML output of an included template. This filter is a progressive enhancement + * intended for applications such as optimizing markup to improve frontend page load performance. Sites must not + * depend on this filter applying since they may opt to stream the responses instead. Callbacks for this filter are + * highly discouraged from using regular expressions to do any kind of replacement on the output. Use the HTML API + * (either `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use {@see DOM\HtmlDocument} as of PHP 8.4 which + * fully supports HTML5. + * + * @since 6.9.0 + * + * @param string $filtered_output HTML template enhancement output buffer. + * @param string $output Original HTML template output buffer. + */ + $filtered_output = (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output ); + } catch ( Exception $exception ) { + $error_log[] = array( + 'level' => E_USER_ERROR, + 'message' => $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + ); + + // Emit to the error log. + trigger_error( + sprintf( + /* translators: %s is wp_template_enhancement_output_buffer */ + __( 'Exception thrown during %s filter: ' ) . $exception->getMessage(), + 'wp_template_enhancement_output_buffer' + ), + E_USER_WARNING + ); + } if ( $display_errors ) { foreach ( $error_log as $error ) { @@ -1025,23 +1043,37 @@ static function ( int $level, string $message, ?string $file = null, ?int $line } } - /** - * Fires at the last moment HTTP headers may be sent. - * - * This happens immediately before the template enhancement output buffer is flushed. This is in contrast with - * the {@see 'send_headers'} action which fires after the initial headers have been sent before the template - * has begun rendering, and thus does not depend on output buffering. This action does not fire if the "template - * enhancement output buffer" was not started. This output buffer is automatically started if this action is added - * before {@see wp_start_template_enhancement_output_buffer()} runs at the {@see 'wp_before_include_template'} - * action with priority 1000. Before this point, the output buffer will also be started automatically if there was a - * {@see 'wp_template_enhancement_output_buffer'} filter added, or if the - * {@see 'wp_should_output_buffer_template_for_enhancement'} filter is made to return `true`. - * - * @since 6.9.0 - * - * @param string $output Output buffer. - */ - do_action( 'wp_send_late_headers', $filtered_output ); + try { + /** + * Fires at the last moment HTTP headers may be sent. + * + * This happens immediately before the template enhancement output buffer is flushed. This is in contrast with + * the {@see 'send_headers'} action which fires after the initial headers have been sent before the template + * has begun rendering, and thus does not depend on output buffering. This action does not fire if the "template + * enhancement output buffer" was not started. This output buffer is automatically started if this action is added + * before {@see wp_start_template_enhancement_output_buffer()} runs at the {@see 'wp_before_include_template'} + * action with priority 1000. Before this point, the output buffer will also be started automatically if there was a + * {@see 'wp_template_enhancement_output_buffer'} filter added, or if the + * {@see 'wp_should_output_buffer_template_for_enhancement'} filter is made to return `true`. + * + * @since 6.9.0 + * + * @param string $output Output buffer. + */ + do_action( 'wp_send_late_headers', $filtered_output ); + } catch ( Exception $exception ) { + // Emit to the error log. + trigger_error( + sprintf( + /* translators: %s is wp_send_late_headers */ + __( 'Exception thrown during %s action: ' ) . $exception->getMessage(), + 'wp_send_late_headers' + ), + E_USER_WARNING + ); + + // TODO: Should this also append the error to $filtered output if $display_errors? But it could make a sent header incorrect. + } if ( $display_errors ) { restore_error_handler(); From 39c6d929fe0c797ab831c316907371c7624cbff4 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 11:49:40 -0700 Subject: [PATCH 05/22] Improve error handling and add tests --- src/wp-includes/template.php | 52 +++-- tests/phpunit/tests/template.php | 314 ++++++++++++++++++++++++++++++- 2 files changed, 331 insertions(+), 35 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 9174acb6917d5..b324098814a6e 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -965,18 +965,23 @@ function wp_finalize_template_enhancement_output_buffer( string $output, int $ph $filtered_output = $output; - $error_log = array(); + $error_log = array(); + set_error_handler( + static function ( int $level, string $message, ?string $file = null, ?int $line = null ) use ( &$error_log ) { + // Switch a user error to an exception so that it can be caught and the buffer can be returned. + if ( E_USER_ERROR === $level ) { + throw new Exception( __( 'User error triggered:' ) . ' ' . $message ); + } + + if ( error_reporting() & $level ) { + $error_log[] = compact( 'level', 'message', 'file', 'line' ); + } + return false; + } + ); $display_errors = ini_get( 'display_errors' ); if ( $display_errors ) { ini_set( 'display_errors', 0 ); - set_error_handler( - static function ( int $level, string $message, ?string $file = null, ?int $line = null, ?array $context = null ) use ( &$error_log ) { - if ( error_reporting() & $level ) { - $error_log[] = compact( 'level', 'message', 'file', 'line', 'context' ); - } - return false; - } - ); } try { @@ -1001,20 +1006,13 @@ static function ( int $level, string $message, ?string $file = null, ?int $line */ $filtered_output = (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output ); } catch ( Exception $exception ) { - $error_log[] = array( - 'level' => E_USER_ERROR, - 'message' => $exception->getMessage(), - 'file' => $exception->getFile(), - 'line' => $exception->getLine(), - ); - // Emit to the error log. trigger_error( sprintf( - /* translators: %s is wp_template_enhancement_output_buffer */ - __( 'Exception thrown during %s filter: ' ) . $exception->getMessage(), - 'wp_template_enhancement_output_buffer' - ), + /* translators: %s is the exception class name */ + __( 'Uncaught exception "%s" thrown:' ), + get_class( $exception ) + ) . ' ' . $exception->getMessage(), E_USER_WARNING ); } @@ -1034,14 +1032,12 @@ static function ( int $level, string $message, ?string $file = null, ?int $line default: $type = 'Error'; } - $displayed_error = sprintf( "
\n%s: %s", $type, $error['message'] ); - if ( null !== $error['file'] ) { - $displayed_error .= sprintf( ' in %s', $error['file'] ); - if ( null !== $error['line'] ) { - $displayed_error .= sprintf( ' on line %d', $error['line'] ); - } + $format = "
\n%s: %s in %s on line %d
"; + if ( ! ini_get( 'html_errors' ) ) { + $format = strip_tags( $format ); } - $displayed_error .= '
'; + + $displayed_error = sprintf( $format, $type, $error['message'], $error['file'], $error['line'] ); $filtered_output .= $displayed_error; } @@ -1086,9 +1082,9 @@ static function ( int $level, string $message, ?string $file = null, ?int $line } if ( $display_errors ) { - restore_error_handler(); ini_set( 'display_errors', 1 ); } + restore_error_handler(); return $filtered_output; } diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index f3dd93ec4fb2b..704fdf15934e8 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -63,11 +63,6 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { ); } - /** - * @var string - */ - protected $original_default_mimetype; - /** * @var WP_Scripts|null */ @@ -83,9 +78,14 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { */ protected $original_theme_features; + /** + * @var array + */ + protected $original_ini_config; + public function set_up() { parent::set_up(); - $this->original_default_mimetype = ini_get( 'default_mimetype' ); + register_post_type( 'cpt', array( @@ -117,6 +117,9 @@ public function set_up() { wp_styles(); $this->original_theme_features = $GLOBALS['_wp_theme_features']; + foreach ( array( 'display_errors', 'error_reporting', 'log_errors', 'error_log', 'default_mimetype', 'html_errors' ) as $config ) { + $this->original_ini_config[ $config ] = ini_get( $config ); + } } public function tear_down() { @@ -125,8 +128,10 @@ public function tear_down() { $wp_styles = $this->original_wp_styles; $GLOBALS['_wp_theme_features'] = $this->original_theme_features; + foreach ( $this->original_ini_config as $config => $value ) { + ini_set( $config, $value ); + } - ini_set( 'default_mimetype', $this->original_default_mimetype ); unregister_post_type( 'cpt' ); unregister_taxonomy( 'taxo' ); $this->set_permalink_structure( '' ); @@ -977,6 +982,301 @@ public function test_wp_start_template_enhancement_output_buffer_for_json(): voi $this->assertSame( $output, $action_args[0], 'Expected the arg passed to wp_finalized_template_enhancement_output_buffer to be the same as the processed output buffer.' ); } + /** + * Data provider for data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_filtering. + * + * @return array + */ + public function data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_filtering(): array { + $log_and_display_all = array( + 'error_reporting' => E_ALL, + 'display_errors' => true, + 'log_errors' => true, + 'html_errors' => true, + ); + + $tests = array( + 'deprecated' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_errors' => static function () { + trigger_error( 'You are history.', E_USER_DEPRECATED ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Deprecated: You are history. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Deprecated: You are history. in __FILE__ on line __LINE__', + ), + ), + 'notice' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_errors' => static function () { + trigger_error( 'POSTED: No trespassing.', E_USER_NOTICE ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Notice: POSTED: No trespassing. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Notice: POSTED: No trespassing. in __FILE__ on line __LINE__', + ), + ), + 'warning' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_errors' => static function () { + trigger_error( 'AVISO: Piso mojado.', E_USER_WARNING ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Warning: AVISO: Piso mojado. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Warning: AVISO: Piso mojado. in __FILE__ on line __LINE__', + ), + ), + 'error' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_errors' => static function () { + @trigger_error( 'ERROR: Can this mistake be rectified?', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + }, + 'expected_processed' => false, + 'expected_error_log' => array( + 'PHP Warning: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified? in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Warning: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified? in __FILE__ on line __LINE__', + ), + ), + 'exception' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_errors' => static function () { + throw new Exception( 'I take exception to this!' ); + }, + 'expected_processed' => false, + 'expected_error_log' => array( + 'PHP Warning: Uncaught exception "Exception" thrown: I take exception to this! in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Warning: Uncaught exception "Exception" thrown: I take exception to this! in __FILE__ on line __LINE__', + ), + ), + 'multiple_non_errors' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_errors' => static function () { + trigger_error( 'You are history.', E_USER_DEPRECATED ); + trigger_error( 'POSTED: No trespassing.', E_USER_NOTICE ); + trigger_error( 'AVISO: Piso mojado.', E_USER_WARNING ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Deprecated: You are history. in __FILE__ on line __LINE__', + 'PHP Notice: POSTED: No trespassing. in __FILE__ on line __LINE__', + 'PHP Warning: AVISO: Piso mojado. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Deprecated: You are history. in __FILE__ on line __LINE__', + 'Notice: POSTED: No trespassing. in __FILE__ on line __LINE__', + 'Warning: AVISO: Piso mojado. in __FILE__ on line __LINE__', + ), + ), + 'deprecated_without_html' => array( + 'ini_config_options' => array_merge( + $log_and_display_all, + array( + 'html_errors' => false, + ) + ), + 'emit_errors' => static function () { + trigger_error( 'You are history.', E_USER_DEPRECATED ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Deprecated: You are history. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Deprecated: You are history. in __FILE__ on line __LINE__', + ), + ), + 'warning_in_eval' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_errors' => static function () { + eval( "trigger_error( 'AVISO: Piso mojado.', E_USER_WARNING );" ); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- We're in a test! + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Warning: AVISO: Piso mojado. in __FILE__ : eval()\'d code on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Warning: AVISO: Piso mojado. in __FILE__ : eval()\'d code on line __LINE__', + ), + ), + ); + + $tests_error_reporting_warnings_and_above = array(); + foreach ( $tests as $name => $test ) { + $test['ini_config_options']['error_reporting'] = E_ALL ^ E_USER_NOTICE ^ E_USER_DEPRECATED; + + $test['expected_error_log'] = array_values( + array_filter( + $test['expected_error_log'], + static function ( $log_entry ) { + return ! ( str_contains( $log_entry, 'Notice' ) || str_contains( $log_entry, 'Deprecated' ) ); + } + ) + ); + + $test['expected_displayed_errors'] = array_values( + array_filter( + $test['expected_displayed_errors'], + static function ( $log_entry ) { + return ! ( str_contains( $log_entry, 'Notice' ) || str_contains( $log_entry, 'Deprecated' ) ); + } + ) + ); + + $tests_error_reporting_warnings_and_above[ "{$name}_with_warnings_and_above_reported" ] = $test; + } + + $tests_without_display_errors = array(); + foreach ( $tests as $name => $test ) { + $test['ini_config_options']['display_errors'] = false; + $test['expected_displayed_errors'] = array(); + + $tests_without_display_errors[ "{$name}_without_display_errors" ] = $test; + } + + $tests_without_display_or_log_errors = array(); + foreach ( $tests as $name => $test ) { + $test['ini_config_options']['display_errors'] = false; + $test['ini_config_options']['log_errors'] = false; + $test['expected_displayed_errors'] = array(); + $test['expected_error_log'] = array(); + + $tests_without_display_or_log_errors[ "{$name}_without_display_errors_or_log_errors" ] = $test; + } + + return array_merge( $tests, $tests_error_reporting_warnings_and_above, $tests_without_display_errors, $tests_without_display_or_log_errors ); + } + + /** + * Tests that errors emitted when filtering wp_template_enhancement_output_buffer are handled as expected. + * + * @ticket 43258 + * @ticket 64108 + * + * @covers ::wp_finalize_template_enhancement_output_buffer + * + * @dataProvider data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_filtering + */ + public function test_wp_finalize_template_enhancement_output_buffer_with_errors_while_filtering( array $ini_config_options, Closure $emit_errors, bool $expected_processed, array $expected_error_log, array $expected_displayed_errors ): void { + // Start a wrapper output buffer so that we can flush the inner buffer. + ob_start(); + + ini_set( 'error_log', $this->temp_filename() ); // phpcs:ignore WordPress.PHP.IniSet.log_errors_Blacklisted, WordPress.PHP.IniSet.Risky + foreach ( $ini_config_options as $config => $option ) { + ini_set( $config, $option ); + } + + add_filter( + 'wp_template_enhancement_output_buffer', + static function ( string $buffer ) use ( $emit_errors ): string { + $buffer = str_replace( 'Hello', 'Goodbye', $buffer ); + $emit_errors(); + return $buffer; + } + ); + + $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' ); + + ?> + + + + Greeting + + +

Hello World!

+ + + assertStringContainsString( 'Goodbye', $processed_output, 'Expected the output buffer to have been processed.' ); + } else { + $this->assertStringNotContainsString( 'Goodbye', $processed_output, 'Expected the output buffer to not have been processed.' ); + } + + $actual_error_log = array_values( + array_map( + static function ( string $error_log_entry ): string { + $error_log_entry = preg_replace( + '/^\[.+?] /', + '', + $error_log_entry + ); + $error_log_entry = preg_replace( + '#(?<= in ).+?' . preg_quote( basename( __FILE__ ), '#' ) . '(\(\d+\))?#', + '__FILE__', + $error_log_entry + ); + return preg_replace( + '#(?<= on line )\d+#', + '__LINE__', + $error_log_entry + ); + }, + array_filter( explode( "\n", trim( file_get_contents( ini_get( 'error_log' ) ) ) ) ) + ) + ); + + $this->assertSame( + $expected_error_log, + $actual_error_log, + 'Expected same error log entries. Snapshot: ' . var_export( $actual_error_log, true ) + ); + + $displayed_errors = array_values( + array_map( + static function ( string $displayed_error ): string { + $displayed_error = str_replace( '
', '', $displayed_error ); + $displayed_error = preg_replace( + '#( in (?:)?).+?' . preg_quote( basename( __FILE__ ), '#' ) . '(\(\d+\))?#', + '$1__FILE__', + $displayed_error + ); + return preg_replace( + '#( on line (?:)?)\d+#', + '$1__LINE__', + $displayed_error + ); + }, + array_filter( + explode( "\n", trim( $processed_output ) ), + static function ( $line ): bool { + return str_contains( $line, ' in ' ); + } + ) + ) + ); + + $this->assertSame( + $expected_displayed_errors, + $displayed_errors, + 'Expected the displayed errors to be the same. Snapshot: ' . var_export( $displayed_errors, true ) + ); + + if ( count( $expected_displayed_errors ) > 0 ) { + $this->assertStringEndsNotWith( '', rtrim( $processed_output ), 'Expected the output to have the error displayed.' ); + } else { + $this->assertStringEndsWith( '', rtrim( $processed_output ), 'Expected the output to not have the error displayed.' ); + } + } + /** * Tests that wp_load_classic_theme_block_styles_on_demand() does not add hooks for block themes. * From ef1b4b437e47582a921e71174131bea4cce1ecbe Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 11:56:53 -0700 Subject: [PATCH 06/22] Display exceptions and user errors as errors not warnings --- src/wp-includes/template.php | 19 +++++++++++++------ tests/phpunit/tests/template.php | 4 ++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index b324098814a6e..48a1ea4cd0ebb 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -965,14 +965,21 @@ function wp_finalize_template_enhancement_output_buffer( string $output, int $ph $filtered_output = $output; + $did_just_catch_exception = false; + $error_log = array(); set_error_handler( - static function ( int $level, string $message, ?string $file = null, ?int $line = null ) use ( &$error_log ) { + static function ( int $level, string $message, ?string $file = null, ?int $line = null ) use ( &$error_log, &$did_just_catch_exception ) { // Switch a user error to an exception so that it can be caught and the buffer can be returned. if ( E_USER_ERROR === $level ) { throw new Exception( __( 'User error triggered:' ) . ' ' . $message ); } + // Display a caught exception as an error since it prevents any of the output buffer filters from applying. + if ( $did_just_catch_exception ) { + $level = E_USER_ERROR; + } + if ( error_reporting() & $level ) { $error_log[] = compact( 'level', 'message', 'file', 'line' ); } @@ -1006,7 +1013,9 @@ static function ( int $level, string $message, ?string $file = null, ?int $line */ $filtered_output = (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output ); } catch ( Exception $exception ) { - // Emit to the error log. + $did_just_catch_exception = true; + + // Emit to the error log as a warning not as an error to prevent halting execution. trigger_error( sprintf( /* translators: %s is the exception class name */ @@ -1016,6 +1025,7 @@ static function ( int $level, string $message, ?string $file = null, ?int $line E_USER_WARNING ); } + $did_just_catch_exception = false; if ( $display_errors ) { foreach ( $error_log as $error ) { @@ -1036,10 +1046,7 @@ static function ( int $level, string $message, ?string $file = null, ?int $line if ( ! ini_get( 'html_errors' ) ) { $format = strip_tags( $format ); } - - $displayed_error = sprintf( $format, $type, $error['message'], $error['file'], $error['line'] ); - - $filtered_output .= $displayed_error; + $filtered_output .= sprintf( $format, $type, $error['message'], $error['file'], $error['line'] ); } } diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 704fdf15934e8..526c005a37e10 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1045,7 +1045,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'PHP Warning: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified? in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Warning: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified? in __FILE__ on line __LINE__', + 'Error: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified? in __FILE__ on line __LINE__', ), ), 'exception' => array( @@ -1058,7 +1058,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'PHP Warning: Uncaught exception "Exception" thrown: I take exception to this! in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Warning: Uncaught exception "Exception" thrown: I take exception to this! in __FILE__ on line __LINE__', + 'Error: Uncaught exception "Exception" thrown: I take exception to this! in __FILE__ on line __LINE__', ), ), 'multiple_non_errors' => array( From 4d7a459ac7d647dc65a10614d8838ce057a6f5a6 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 12:22:00 -0700 Subject: [PATCH 07/22] Suppres false positive for phpstan --- src/wp-includes/template.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 48a1ea4cd0ebb..41db56cc90415 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -976,7 +976,7 @@ static function ( int $level, string $message, ?string $file = null, ?int $line } // Display a caught exception as an error since it prevents any of the output buffer filters from applying. - if ( $did_just_catch_exception ) { + if ( $did_just_catch_exception ) { // @phpstan-ignore if.alwaysFalse (The variable is set in the catch block below.) $level = E_USER_ERROR; } From e6029bb400204a396b5ebfb9fdd0937f25f0e3f2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 12:22:12 -0700 Subject: [PATCH 08/22] Update action name --- src/wp-includes/template.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 41db56cc90415..91137b1d6a281 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -980,6 +980,8 @@ static function ( int $level, string $message, ?string $file = null, ?int $line $level = E_USER_ERROR; } + // TODO: Append to the message that this error happened while doing wp_template_enhancement_output_buffer filters or the wp_finalized_template_enhancement_output_buffer action? + if ( error_reporting() & $level ) { $error_log[] = compact( 'level', 'message', 'file', 'line' ); } @@ -1078,9 +1080,9 @@ static function ( int $level, string $message, ?string $file = null, ?int $line // Emit to the error log. trigger_error( sprintf( - /* translators: %s is wp_send_late_headers */ + /* translators: %s is wp_finalized_template_enhancement_output_buffer */ __( 'Exception thrown during %s action: ' ) . $exception->getMessage(), - 'wp_send_late_headers' + 'wp_finalized_template_enhancement_output_buffer' ), E_USER_WARNING ); From 24b31718ce60f6557a5ec883e49ca72b1877726e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 12:23:22 -0700 Subject: [PATCH 09/22] Improve variable assignment location --- src/wp-includes/template.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 91137b1d6a281..38ba2a7248d45 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -1015,9 +1015,8 @@ static function ( int $level, string $message, ?string $file = null, ?int $line */ $filtered_output = (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output ); } catch ( Exception $exception ) { - $did_just_catch_exception = true; - // Emit to the error log as a warning not as an error to prevent halting execution. + $did_just_catch_exception = true; trigger_error( sprintf( /* translators: %s is the exception class name */ @@ -1026,8 +1025,8 @@ static function ( int $level, string $message, ?string $file = null, ?int $line ) . ' ' . $exception->getMessage(), E_USER_WARNING ); + $did_just_catch_exception = false; } - $did_just_catch_exception = false; if ( $display_errors ) { foreach ( $error_log as $error ) { From 231db4ba16299efcfa9551ca7d4d0744250f9a4f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 12:43:15 -0700 Subject: [PATCH 10/22] Add test coverage for errors in action --- tests/phpunit/tests/template.php | 134 ++++++++++++++++++++----------- 1 file changed, 89 insertions(+), 45 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 526c005a37e10..0ac22dbb74197 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -983,11 +983,11 @@ public function test_wp_start_template_enhancement_output_buffer_for_json(): voi } /** - * Data provider for data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_filtering. + * Data provider for data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_processing. * * @return array */ - public function data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_filtering(): array { + public function data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_processing(): array { $log_and_display_all = array( 'error_reporting' => E_ALL, 'display_errors' => true, @@ -998,86 +998,114 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu $tests = array( 'deprecated' => array( 'ini_config_options' => $log_and_display_all, - 'emit_errors' => static function () { - trigger_error( 'You are history.', E_USER_DEPRECATED ); + 'emit_filter_errors' => static function () { + trigger_error( 'You are history during filter.', E_USER_DEPRECATED ); + }, + 'emit_action_errors' => static function () { + trigger_error( 'You are history during action.', E_USER_DEPRECATED ); }, 'expected_processed' => true, 'expected_error_log' => array( - 'PHP Deprecated: You are history. in __FILE__ on line __LINE__', + 'PHP Deprecated: You are history during filter. in __FILE__ on line __LINE__', + 'PHP Deprecated: You are history during action. in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Deprecated: You are history. in __FILE__ on line __LINE__', + 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', ), ), 'notice' => array( 'ini_config_options' => $log_and_display_all, - 'emit_errors' => static function () { - trigger_error( 'POSTED: No trespassing.', E_USER_NOTICE ); + 'emit_filter_errors' => static function () { + trigger_error( 'POSTED: No trespassing during filter.', E_USER_NOTICE ); + }, + 'emit_action_errors' => static function () { + trigger_error( 'POSTED: No trespassing during action.', E_USER_NOTICE ); }, 'expected_processed' => true, 'expected_error_log' => array( - 'PHP Notice: POSTED: No trespassing. in __FILE__ on line __LINE__', + 'PHP Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'PHP Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Notice: POSTED: No trespassing. in __FILE__ on line __LINE__', + 'Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', ), ), 'warning' => array( 'ini_config_options' => $log_and_display_all, - 'emit_errors' => static function () { - trigger_error( 'AVISO: Piso mojado.', E_USER_WARNING ); + 'emit_filter_errors' => static function () { + trigger_error( 'AVISO: Piso mojado durante filtro.', E_USER_WARNING ); + }, + 'emit_action_errors' => static function () { + trigger_error( 'AVISO: Piso mojado durante acción.', E_USER_WARNING ); }, 'expected_processed' => true, 'expected_error_log' => array( - 'PHP Warning: AVISO: Piso mojado. in __FILE__ on line __LINE__', + 'PHP Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', + 'PHP Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Warning: AVISO: Piso mojado. in __FILE__ on line __LINE__', + 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', ), ), 'error' => array( 'ini_config_options' => $log_and_display_all, - 'emit_errors' => static function () { - @trigger_error( 'ERROR: Can this mistake be rectified?', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + 'emit_filter_errors' => static function () { + @trigger_error( 'ERROR: Can this mistake be rectified during filter?', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + }, + 'emit_action_errors' => static function () { + @trigger_error( 'ERROR: Can this mistake be rectified during action?', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged }, 'expected_processed' => false, 'expected_error_log' => array( - 'PHP Warning: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified? in __FILE__ on line __LINE__', + 'PHP Warning: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__', + 'PHP Warning: Exception thrown during wp_finalized_template_enhancement_output_buffer action: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Error: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified? in __FILE__ on line __LINE__', + 'Error: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__', ), ), 'exception' => array( 'ini_config_options' => $log_and_display_all, - 'emit_errors' => static function () { - throw new Exception( 'I take exception to this!' ); + 'emit_filter_errors' => static function () { + throw new Exception( 'I take exception to this filter!' ); + }, + 'emit_action_errors' => static function () { + throw new Exception( 'I take exception to this action!' ); }, 'expected_processed' => false, 'expected_error_log' => array( - 'PHP Warning: Uncaught exception "Exception" thrown: I take exception to this! in __FILE__ on line __LINE__', + 'PHP Warning: Uncaught exception "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__', + 'PHP Warning: Exception thrown during wp_finalized_template_enhancement_output_buffer action: I take exception to this action! in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Error: Uncaught exception "Exception" thrown: I take exception to this! in __FILE__ on line __LINE__', + 'Error: Uncaught exception "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__', ), ), 'multiple_non_errors' => array( 'ini_config_options' => $log_and_display_all, - 'emit_errors' => static function () { - trigger_error( 'You are history.', E_USER_DEPRECATED ); - trigger_error( 'POSTED: No trespassing.', E_USER_NOTICE ); - trigger_error( 'AVISO: Piso mojado.', E_USER_WARNING ); + 'emit_filter_errors' => static function () { + trigger_error( 'You are history during filter.', E_USER_DEPRECATED ); + trigger_error( 'POSTED: No trespassing during filter.', E_USER_NOTICE ); + trigger_error( 'AVISO: Piso mojado durante filtro.', E_USER_WARNING ); + }, + 'emit_action_errors' => static function () { + trigger_error( 'You are history during action.', E_USER_DEPRECATED ); + trigger_error( 'POSTED: No trespassing during action.', E_USER_NOTICE ); + trigger_error( 'AVISO: Piso mojado durante acción.', E_USER_WARNING ); }, 'expected_processed' => true, 'expected_error_log' => array( - 'PHP Deprecated: You are history. in __FILE__ on line __LINE__', - 'PHP Notice: POSTED: No trespassing. in __FILE__ on line __LINE__', - 'PHP Warning: AVISO: Piso mojado. in __FILE__ on line __LINE__', + 'PHP Deprecated: You are history during filter. in __FILE__ on line __LINE__', + 'PHP Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'PHP Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', + 'PHP Deprecated: You are history during action. in __FILE__ on line __LINE__', + 'PHP Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', + 'PHP Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Deprecated: You are history. in __FILE__ on line __LINE__', - 'Notice: POSTED: No trespassing. in __FILE__ on line __LINE__', - 'Warning: AVISO: Piso mojado. in __FILE__ on line __LINE__', + 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', + 'Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', ), ), 'deprecated_without_html' => array( @@ -1087,28 +1115,33 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'html_errors' => false, ) ), - 'emit_errors' => static function () { - trigger_error( 'You are history.', E_USER_DEPRECATED ); + 'emit_filter_errors' => static function () { + trigger_error( 'You are history during filter.', E_USER_DEPRECATED ); }, + 'emit_action_errors' => null, 'expected_processed' => true, 'expected_error_log' => array( - 'PHP Deprecated: You are history. in __FILE__ on line __LINE__', + 'PHP Deprecated: You are history during filter. in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Deprecated: You are history. in __FILE__ on line __LINE__', + 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', ), ), 'warning_in_eval' => array( 'ini_config_options' => $log_and_display_all, - 'emit_errors' => static function () { - eval( "trigger_error( 'AVISO: Piso mojado.', E_USER_WARNING );" ); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- We're in a test! + 'emit_filter_errors' => static function () { + eval( "trigger_error( 'AVISO: Piso mojado durante filtro.', E_USER_WARNING );" ); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- We're in a test! + }, + 'emit_action_errors' => static function () { + eval( "trigger_error( 'AVISO: Piso mojado durante acción.', E_USER_WARNING );" ); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- We're in a test! }, 'expected_processed' => true, 'expected_error_log' => array( - 'PHP Warning: AVISO: Piso mojado. in __FILE__ : eval()\'d code on line __LINE__', + 'PHP Warning: AVISO: Piso mojado durante filtro. in __FILE__ : eval()\'d code on line __LINE__', + 'PHP Warning: AVISO: Piso mojado durante acción. in __FILE__ : eval()\'d code on line __LINE__', ), 'expected_displayed_errors' => array( - 'Warning: AVISO: Piso mojado. in __FILE__ : eval()\'d code on line __LINE__', + 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ : eval()\'d code on line __LINE__', ), ), ); @@ -1160,16 +1193,16 @@ static function ( $log_entry ) { } /** - * Tests that errors emitted when filtering wp_template_enhancement_output_buffer are handled as expected. + * Tests that errors are handled as expected when errors are emitted when filtering wp_template_enhancement_output_buffer or doing the wp_finalize_template_enhancement_output_buffer action. * * @ticket 43258 * @ticket 64108 * * @covers ::wp_finalize_template_enhancement_output_buffer * - * @dataProvider data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_filtering + * @dataProvider data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_processing */ - public function test_wp_finalize_template_enhancement_output_buffer_with_errors_while_filtering( array $ini_config_options, Closure $emit_errors, bool $expected_processed, array $expected_error_log, array $expected_displayed_errors ): void { + public function test_wp_finalize_template_enhancement_output_buffer_with_errors_while_processing( array $ini_config_options, ?Closure $emit_filter_errors, ?Closure $emit_action_errors, bool $expected_processed, array $expected_error_log, array $expected_displayed_errors ): void { // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); @@ -1180,13 +1213,24 @@ public function test_wp_finalize_template_enhancement_output_buffer_with_errors_ add_filter( 'wp_template_enhancement_output_buffer', - static function ( string $buffer ) use ( $emit_errors ): string { + static function ( string $buffer ) use ( $emit_filter_errors ): string { $buffer = str_replace( 'Hello', 'Goodbye', $buffer ); - $emit_errors(); + if ( $emit_filter_errors ) { + $emit_filter_errors(); + } return $buffer; } ); + if ( $emit_action_errors ) { + add_action( + 'wp_finalized_template_enhancement_output_buffer', + static function () use ( $emit_action_errors ): void { + $emit_action_errors(); + } + ); + } + $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' ); ?> From 5607d30f8609071817048fd8206ade3add2b3c69 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 12:45:34 -0700 Subject: [PATCH 11/22] Harmonize catch blocks --- src/wp-includes/template.php | 12 +++++++----- tests/phpunit/tests/template.php | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 38ba2a7248d45..6f09d8f94da41 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -1076,15 +1076,17 @@ static function ( int $level, string $message, ?string $file = null, ?int $line */ do_action( 'wp_finalized_template_enhancement_output_buffer', $filtered_output ); } catch ( Exception $exception ) { - // Emit to the error log. + // Emit to the error log as a warning not as an error to prevent halting execution. + $did_just_catch_exception = true; trigger_error( sprintf( - /* translators: %s is wp_finalized_template_enhancement_output_buffer */ - __( 'Exception thrown during %s action: ' ) . $exception->getMessage(), - 'wp_finalized_template_enhancement_output_buffer' - ), + /* translators: %s is the exception class name */ + __( 'Uncaught exception "%s" thrown:' ), + get_class( $exception ) + ) . ' ' . $exception->getMessage(), E_USER_WARNING ); + $did_just_catch_exception = false; // TODO: Should this also append the error to $filtered output if $display_errors? But it could make a sent header incorrect. } diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 0ac22dbb74197..081342d75e9f0 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1058,7 +1058,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'expected_processed' => false, 'expected_error_log' => array( 'PHP Warning: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__', - 'PHP Warning: Exception thrown during wp_finalized_template_enhancement_output_buffer action: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', + 'PHP Warning: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( 'Error: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__', @@ -1075,7 +1075,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'expected_processed' => false, 'expected_error_log' => array( 'PHP Warning: Uncaught exception "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__', - 'PHP Warning: Exception thrown during wp_finalized_template_enhancement_output_buffer action: I take exception to this action! in __FILE__ on line __LINE__', + 'PHP Warning: Uncaught exception "Exception" thrown: I take exception to this action! in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( 'Error: Uncaught exception "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__', From bb7aeafa64cbefb5980e27a7a4bc3bd1c25ee07b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 12:59:58 -0700 Subject: [PATCH 12/22] Improve docs about not printing during callbacks --- src/wp-includes/template.php | 42 ++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 6f09d8f94da41..33d3cd552252e 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -999,14 +999,18 @@ static function ( int $level, string $message, ?string $file = null, ?int $line * * This filter only applies the HTML output of an included template. This filter is a progressive enhancement * intended for applications such as optimizing markup to improve frontend page load performance. Sites must not - * depend on this filter applying since they may opt to stream the responses instead. Callbacks for this filter are - * highly discouraged from using regular expressions to do any kind of replacement on the output. Use the HTML API - * (either `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use {@see DOM\HtmlDocument} as of PHP 8.4 which - * fully supports HTML5. + * depend on this filter applying since they may opt to stream the responses instead. Callbacks for this filter + * are highly discouraged from using regular expressions to do any kind of replacement on the output. Use the + * HTML API (either `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use {@see DOM\HtmlDocument} as of + * PHP 8.4 which fully supports HTML5. * - * Important: Because this filter is applied inside an output buffer callback (i.e. display handler), any callbacks - * added to the filter must not attempt to start their own output buffers. Otherwise, PHP will raise a fatal error: - * "Cannot use output buffering in output buffering display handlers." + * Do not print any output during this filter. While filters normally don't print anything, this is especially + * important since this applies during an output buffer callback. Prior to PHP 8.5, the output will be silently + * omitted, whereas afterward a deprecation notice will be emitted. + * + * Important: Because this filter is applied inside an output buffer callback (i.e. display handler), any + * callbacks added to the filter must not attempt to start their own output buffers. Otherwise, PHP will raise a + * fatal error: "Cannot use output buffering in output buffering display handlers." * * @since 6.9.0 * @@ -1055,20 +1059,26 @@ static function ( int $level, string $message, ?string $file = null, ?int $line /** * Fires after the template enhancement output buffer has been finalized. * - * This happens immediately before the template enhancement output buffer is flushed. No output may be printed at - * this action. However, HTTP headers may be sent, which makes this action complimentary to the + * This happens immediately before the template enhancement output buffer is flushed. No output may be printed + * at this action; prior to PHP 8.5, the output will be silently omitted, whereas afterward a deprecation notice + * will be emitted. Nevertheless, HTTP headers may be sent, which makes this action complimentary to the * {@see 'send_headers'} action, in which headers may be sent before the template has started rendering. In * contrast, this `wp_finalized_template_enhancement_output_buffer` action is the possible point at which HTTP - * headers can be sent. This action does not fire if the "template enhancement output buffer" was not started. This - * output buffer is automatically started if this action is added before - * {@see wp_start_template_enhancement_output_buffer()} runs at the {@see 'wp_before_include_template'} action with - * priority 1000. Before this point, the output buffer will also be started automatically if there was a + * headers can be sent. This action does not fire if the "template enhancement output buffer" was not started. + * This output buffer is automatically started if this action is added before + * {@see wp_start_template_enhancement_output_buffer()} runs at the {@see 'wp_before_include_template'} action + * with priority 1000. Before this point, the output buffer will also be started automatically if there was a * {@see 'wp_template_enhancement_output_buffer'} filter added, or if the * {@see 'wp_should_output_buffer_template_for_enhancement'} filter is made to return `true`. * - * Important: Because this action fires inside an output buffer callback (i.e. display handler), any callbacks added - * to the action must not attempt to start their own output buffers. Otherwise, PHP will raise a fatal error: - * "Cannot use output buffering in output buffering display handlers." + * Important: Because this action fires inside an output buffer callback (i.e. display handler), any callbacks + * added to the action must not attempt to start their own output buffers. Otherwise, PHP will raise a fatal + * error: "Cannot use output buffering in output buffering display handlers." + * + * If any errors are occur in callbacks for this action (e.g. deprecations, notices, warnings, exceptions), + * there will be no error message printed even if `display_errors` is enabled. This is because the output has + * already been finalized. The error will be emitted to the error log, however, as long as the error reporting + * level is configured. * * @since 6.9.0 * From e39794eba19dc80911b03f7da780bc3843eaaa5d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 13:08:16 -0700 Subject: [PATCH 13/22] Remove TODO --- src/wp-includes/template.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 33d3cd552252e..32118b500a76a 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -980,8 +980,7 @@ static function ( int $level, string $message, ?string $file = null, ?int $line $level = E_USER_ERROR; } - // TODO: Append to the message that this error happened while doing wp_template_enhancement_output_buffer filters or the wp_finalized_template_enhancement_output_buffer action? - + // Capture a reported error to be displayed by appending to the processed output buffer if display_errors is enabled. if ( error_reporting() & $level ) { $error_log[] = compact( 'level', 'message', 'file', 'line' ); } From 2a359dfee46ce975be8a7db97854bef920d385cc Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 13:11:11 -0700 Subject: [PATCH 14/22] Remove todo --- src/wp-includes/template.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 32118b500a76a..36cce1ccb7503 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -1096,8 +1096,6 @@ static function ( int $level, string $message, ?string $file = null, ?int $line E_USER_WARNING ); $did_just_catch_exception = false; - - // TODO: Should this also append the error to $filtered output if $display_errors? But it could make a sent header incorrect. } if ( $display_errors ) { From dd9bbdf821f69d842ba82c8fc60476bf68fc67e3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 14:17:09 -0700 Subject: [PATCH 15/22] Opt to display errors which occur during finalize action --- src/wp-includes/template.php | 51 ++++++++++++++------------------ tests/phpunit/tests/template.php | 9 ++++++ 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 36cce1ccb7503..dceb7a4e062e8 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -1031,29 +1031,6 @@ static function ( int $level, string $message, ?string $file = null, ?int $line $did_just_catch_exception = false; } - if ( $display_errors ) { - foreach ( $error_log as $error ) { - switch ( $error['level'] ) { - case E_USER_NOTICE: - $type = 'Notice'; - break; - case E_USER_DEPRECATED: - $type = 'Deprecated'; - break; - case E_USER_WARNING: - $type = 'Warning'; - break; - default: - $type = 'Error'; - } - $format = "
\n%s: %s in %s on line %d
"; - if ( ! ini_get( 'html_errors' ) ) { - $format = strip_tags( $format ); - } - $filtered_output .= sprintf( $format, $type, $error['message'], $error['file'], $error['line'] ); - } - } - try { /** * Fires after the template enhancement output buffer has been finalized. @@ -1074,11 +1051,6 @@ static function ( int $level, string $message, ?string $file = null, ?int $line * added to the action must not attempt to start their own output buffers. Otherwise, PHP will raise a fatal * error: "Cannot use output buffering in output buffering display handlers." * - * If any errors are occur in callbacks for this action (e.g. deprecations, notices, warnings, exceptions), - * there will be no error message printed even if `display_errors` is enabled. This is because the output has - * already been finalized. The error will be emitted to the error log, however, as long as the error reporting - * level is configured. - * * @since 6.9.0 * * @param string $output Finalized output buffer. @@ -1098,9 +1070,32 @@ static function ( int $level, string $message, ?string $file = null, ?int $line $did_just_catch_exception = false; } + // Append any errors to be displayed before returning flushing the buffer. if ( $display_errors ) { + foreach ( $error_log as $error ) { + switch ( $error['level'] ) { + case E_USER_NOTICE: + $type = 'Notice'; + break; + case E_USER_DEPRECATED: + $type = 'Deprecated'; + break; + case E_USER_WARNING: + $type = 'Warning'; + break; + default: + $type = 'Error'; + } + $format = "
\n%s: %s in %s on line %d
"; + if ( ! ini_get( 'html_errors' ) ) { + $format = strip_tags( $format ); + } + $filtered_output .= sprintf( $format, $type, $error['message'], $error['file'], $error['line'] ); + } + ini_set( 'display_errors', 1 ); } + restore_error_handler(); return $filtered_output; diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 081342d75e9f0..4c1bceb06f1f5 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1011,6 +1011,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu ), 'expected_displayed_errors' => array( 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', + 'Deprecated: You are history during action. in __FILE__ on line __LINE__', ), ), 'notice' => array( @@ -1028,6 +1029,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu ), 'expected_displayed_errors' => array( 'Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', ), ), 'warning' => array( @@ -1045,6 +1047,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu ), 'expected_displayed_errors' => array( 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', + 'Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', ), ), 'error' => array( @@ -1062,6 +1065,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu ), 'expected_displayed_errors' => array( 'Error: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__', + 'Error: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', ), ), 'exception' => array( @@ -1079,6 +1083,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu ), 'expected_displayed_errors' => array( 'Error: Uncaught exception "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__', + 'Error: Uncaught exception "Exception" thrown: I take exception to this action! in __FILE__ on line __LINE__', ), ), 'multiple_non_errors' => array( @@ -1106,6 +1111,9 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', 'Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', + 'Deprecated: You are history during action. in __FILE__ on line __LINE__', + 'Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', + 'Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', ), ), 'deprecated_without_html' => array( @@ -1142,6 +1150,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu ), 'expected_displayed_errors' => array( 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ : eval()\'d code on line __LINE__', + 'Warning: AVISO: Piso mojado durante acción. in __FILE__ : eval()\'d code on line __LINE__', ), ), ); From 2fe5542103f1a07f6c6365bd8bbaca7048aa4b9e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 19:49:13 -0700 Subject: [PATCH 16/22] Improve handling of html_errors and add support for error_prepend_string/error_append_string --- src/wp-includes/template.php | 15 +++++++--- tests/phpunit/tests/template.php | 48 ++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index dceb7a4e062e8..ef7b7d3ddc08a 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -1086,11 +1086,18 @@ static function ( int $level, string $message, ?string $file = null, ?int $line default: $type = 'Error'; } - $format = "
\n%s: %s in %s on line %d
"; - if ( ! ini_get( 'html_errors' ) ) { - $format = strip_tags( $format ); + + if ( ini_get( 'html_errors' ) ) { + /* + * Adapted from PHP internals: . + * The self-closing tags are a vestige of the XHTML past! + */ + $format = "%s
\n%s: %s in %s on line %s
\n%s"; + } else { + // Adapted from PHP internals: . + $format = "%s\n%s: %s in %s on line %s\n%s"; } - $filtered_output .= sprintf( $format, $type, $error['message'], $error['file'], $error['line'] ); + $filtered_output .= sprintf( $format, ini_get( 'error_prepend_string' ), $type, $error['message'], $error['file'], $error['line'], ini_get( 'error_append_string' ) ); } ini_set( 'display_errors', 1 ); diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 4c1bceb06f1f5..227b0f66d07b1 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -117,7 +117,7 @@ public function set_up() { wp_styles(); $this->original_theme_features = $GLOBALS['_wp_theme_features']; - foreach ( array( 'display_errors', 'error_reporting', 'log_errors', 'error_log', 'default_mimetype', 'html_errors' ) as $config ) { + foreach ( array( 'display_errors', 'error_reporting', 'log_errors', 'error_log', 'default_mimetype', 'html_errors', 'error_prepend_string', 'error_append_string' ) as $config ) { $this->original_ini_config[ $config ] = ini_get( $config ); } } @@ -1010,8 +1010,8 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'PHP Deprecated: You are history during action. in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', - 'Deprecated: You are history during action. in __FILE__ on line __LINE__', + 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', + 'Deprecated: You are history during action. in __FILE__ on line __LINE__', ), ), 'notice' => array( @@ -1028,8 +1028,8 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'PHP Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', - 'Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', + 'Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', ), ), 'warning' => array( @@ -1046,8 +1046,8 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'PHP Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', - 'Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', + 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', + 'Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', ), ), 'error' => array( @@ -1064,8 +1064,8 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'PHP Warning: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Error: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__', - 'Error: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', + 'Error: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__', + 'Error: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', ), ), 'exception' => array( @@ -1082,8 +1082,8 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'PHP Warning: Uncaught exception "Exception" thrown: I take exception to this action! in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Error: Uncaught exception "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__', - 'Error: Uncaught exception "Exception" thrown: I take exception to this action! in __FILE__ on line __LINE__', + 'Error: Uncaught exception "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__', + 'Error: Uncaught exception "Exception" thrown: I take exception to this action! in __FILE__ on line __LINE__', ), ), 'multiple_non_errors' => array( @@ -1108,12 +1108,12 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'PHP Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', - 'Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', - 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', - 'Deprecated: You are history during action. in __FILE__ on line __LINE__', - 'Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', - 'Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', + 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', + 'Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', + 'Deprecated: You are history during action. in __FILE__ on line __LINE__', + 'Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', + 'Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', ), ), 'deprecated_without_html' => array( @@ -1135,8 +1135,14 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', ), ), - 'warning_in_eval' => array( - 'ini_config_options' => $log_and_display_all, + 'warning_in_eval_with_prepend_and_append' => array( + 'ini_config_options' => array_merge( + $log_and_display_all, + array( + 'error_prepend_string' => '
PHP Problem!', + 'error_append_string' => '
', + ) + ), 'emit_filter_errors' => static function () { eval( "trigger_error( 'AVISO: Piso mojado durante filtro.', E_USER_WARNING );" ); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- We're in a test! }, @@ -1149,8 +1155,8 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'PHP Warning: AVISO: Piso mojado durante acción. in __FILE__ : eval()\'d code on line __LINE__', ), 'expected_displayed_errors' => array( - 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ : eval()\'d code on line __LINE__', - 'Warning: AVISO: Piso mojado durante acción. in __FILE__ : eval()\'d code on line __LINE__', + 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ : eval()\'d code on line __LINE__', + 'Warning: AVISO: Piso mojado durante acción. in __FILE__ : eval()\'d code on line __LINE__', ), ), ); From cc31d802fe9e3bfc2782eb6267a2ed066935dd9e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 19:51:45 -0700 Subject: [PATCH 17/22] Fix phpcs --- tests/phpunit/tests/template.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 227b0f66d07b1..b499d444d685d 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -996,7 +996,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu ); $tests = array( - 'deprecated' => array( + 'deprecated' => array( 'ini_config_options' => $log_and_display_all, 'emit_filter_errors' => static function () { trigger_error( 'You are history during filter.', E_USER_DEPRECATED ); @@ -1014,7 +1014,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'Deprecated: You are history during action. in __FILE__ on line __LINE__', ), ), - 'notice' => array( + 'notice' => array( 'ini_config_options' => $log_and_display_all, 'emit_filter_errors' => static function () { trigger_error( 'POSTED: No trespassing during filter.', E_USER_NOTICE ); @@ -1032,7 +1032,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', ), ), - 'warning' => array( + 'warning' => array( 'ini_config_options' => $log_and_display_all, 'emit_filter_errors' => static function () { trigger_error( 'AVISO: Piso mojado durante filtro.', E_USER_WARNING ); @@ -1050,7 +1050,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', ), ), - 'error' => array( + 'error' => array( 'ini_config_options' => $log_and_display_all, 'emit_filter_errors' => static function () { @trigger_error( 'ERROR: Can this mistake be rectified during filter?', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged @@ -1068,7 +1068,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'Error: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', ), ), - 'exception' => array( + 'exception' => array( 'ini_config_options' => $log_and_display_all, 'emit_filter_errors' => static function () { throw new Exception( 'I take exception to this filter!' ); @@ -1086,7 +1086,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'Error: Uncaught exception "Exception" thrown: I take exception to this action! in __FILE__ on line __LINE__', ), ), - 'multiple_non_errors' => array( + 'multiple_non_errors' => array( 'ini_config_options' => $log_and_display_all, 'emit_filter_errors' => static function () { trigger_error( 'You are history during filter.', E_USER_DEPRECATED ); @@ -1116,7 +1116,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', ), ), - 'deprecated_without_html' => array( + 'deprecated_without_html' => array( 'ini_config_options' => array_merge( $log_and_display_all, array( @@ -1135,7 +1135,7 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', ), ), - 'warning_in_eval_with_prepend_and_append' => array( + 'warning_in_eval_with_prepend_and_append' => array( 'ini_config_options' => array_merge( $log_and_display_all, array( From 5b13a71d53f42e95703ac624f547ce6b0654a6fe Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 20:03:27 -0700 Subject: [PATCH 18/22] Account for display_errors being stderr Co-authored-by: Dennis Snell --- src/wp-includes/template.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index ef7b7d3ddc08a..ed4cdac69db0a 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -987,8 +987,8 @@ static function ( int $level, string $message, ?string $file = null, ?int $line return false; } ); - $display_errors = ini_get( 'display_errors' ); - if ( $display_errors ) { + $original_display_errors = ini_get( 'display_errors' ); + if ( $original_display_errors ) { ini_set( 'display_errors', 0 ); } @@ -1071,7 +1071,7 @@ static function ( int $level, string $message, ?string $file = null, ?int $line } // Append any errors to be displayed before returning flushing the buffer. - if ( $display_errors ) { + if ( $original_display_errors && 'stderr' !== $original_display_errors ) { foreach ( $error_log as $error ) { switch ( $error['level'] ) { case E_USER_NOTICE: @@ -1100,7 +1100,7 @@ static function ( int $level, string $message, ?string $file = null, ?int $line $filtered_output .= sprintf( $format, ini_get( 'error_prepend_string' ), $type, $error['message'], $error['file'], $error['line'], ini_get( 'error_append_string' ) ); } - ini_set( 'display_errors', 1 ); + ini_set( 'display_errors', $original_display_errors ); } restore_error_handler(); From 2ef5af19e5a2677d9015b93f0643cd34c497e818 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 20:06:53 -0700 Subject: [PATCH 19/22] Add test case for display_errors=stderr --- tests/phpunit/tests/template.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index b499d444d685d..8eb8f60091569 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1159,6 +1159,26 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu 'Warning: AVISO: Piso mojado durante acción. in __FILE__ : eval()\'d code on line __LINE__', ), ), + 'notice_with_display_errors_stderr' => array( + 'ini_config_options' => array_merge( + $log_and_display_all, + array( + 'display_errors' => 'stderr', + ) + ), + 'emit_filter_errors' => static function () { + trigger_error( 'POSTED: No trespassing during filter.' ); + }, + 'emit_action_errors' => static function () { + trigger_error( 'POSTED: No trespassing during action.' ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'PHP Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array(), + ), ); $tests_error_reporting_warnings_and_above = array(); From 2d9bdd45ea65a761c8cb28fd1e5ff65cd5a44329 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 1 Nov 2025 20:13:03 -0700 Subject: [PATCH 20/22] Use Throwable instead of Exception when catching Co-authored-by: Dennis Snell --- src/wp-includes/template.php | 34 ++++++++++++++++---------------- tests/phpunit/tests/template.php | 16 +++++++-------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index ed4cdac69db0a..a8ee4cbf45033 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -965,18 +965,18 @@ function wp_finalize_template_enhancement_output_buffer( string $output, int $ph $filtered_output = $output; - $did_just_catch_exception = false; + $did_just_catch = false; $error_log = array(); set_error_handler( - static function ( int $level, string $message, ?string $file = null, ?int $line = null ) use ( &$error_log, &$did_just_catch_exception ) { + static function ( int $level, string $message, ?string $file = null, ?int $line = null ) use ( &$error_log, &$did_just_catch ) { // Switch a user error to an exception so that it can be caught and the buffer can be returned. if ( E_USER_ERROR === $level ) { throw new Exception( __( 'User error triggered:' ) . ' ' . $message ); } // Display a caught exception as an error since it prevents any of the output buffer filters from applying. - if ( $did_just_catch_exception ) { // @phpstan-ignore if.alwaysFalse (The variable is set in the catch block below.) + if ( $did_just_catch ) { // @phpstan-ignore if.alwaysFalse (The variable is set in the catch block below.) $level = E_USER_ERROR; } @@ -1017,18 +1017,18 @@ static function ( int $level, string $message, ?string $file = null, ?int $line * @param string $output Original HTML template output buffer. */ $filtered_output = (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output ); - } catch ( Exception $exception ) { + } catch ( Throwable $throwable ) { // Emit to the error log as a warning not as an error to prevent halting execution. - $did_just_catch_exception = true; + $did_just_catch = true; trigger_error( sprintf( - /* translators: %s is the exception class name */ - __( 'Uncaught exception "%s" thrown:' ), - get_class( $exception ) - ) . ' ' . $exception->getMessage(), + /* translators: %s is the throwable class name */ + __( 'Uncaught "%s" thrown:' ), + get_class( $throwable ) + ) . ' ' . $throwable->getMessage(), E_USER_WARNING ); - $did_just_catch_exception = false; + $did_just_catch = false; } try { @@ -1056,18 +1056,18 @@ static function ( int $level, string $message, ?string $file = null, ?int $line * @param string $output Finalized output buffer. */ do_action( 'wp_finalized_template_enhancement_output_buffer', $filtered_output ); - } catch ( Exception $exception ) { + } catch ( Throwable $throwable ) { // Emit to the error log as a warning not as an error to prevent halting execution. - $did_just_catch_exception = true; + $did_just_catch = true; trigger_error( sprintf( - /* translators: %s is the exception class name */ - __( 'Uncaught exception "%s" thrown:' ), - get_class( $exception ) - ) . ' ' . $exception->getMessage(), + /* translators: %s is the class name */ + __( 'Uncaught "%s" thrown:' ), + get_class( $throwable ) + ) . ' ' . $throwable->getMessage(), E_USER_WARNING ); - $did_just_catch_exception = false; + $did_just_catch = false; } // Append any errors to be displayed before returning flushing the buffer. diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 8eb8f60091569..2ff08518b4788 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -1060,12 +1060,12 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu }, 'expected_processed' => false, 'expected_error_log' => array( - 'PHP Warning: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__', - 'PHP Warning: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', + 'PHP Warning: Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__', + 'PHP Warning: Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Error: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__', - 'Error: Uncaught exception "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', + 'Error: Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__', + 'Error: Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', ), ), 'exception' => array( @@ -1078,12 +1078,12 @@ public function data_provider_to_test_wp_finalize_template_enhancement_output_bu }, 'expected_processed' => false, 'expected_error_log' => array( - 'PHP Warning: Uncaught exception "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__', - 'PHP Warning: Uncaught exception "Exception" thrown: I take exception to this action! in __FILE__ on line __LINE__', + 'PHP Warning: Uncaught "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__', + 'PHP Warning: Uncaught "Exception" thrown: I take exception to this action! in __FILE__ on line __LINE__', ), 'expected_displayed_errors' => array( - 'Error: Uncaught exception "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__', - 'Error: Uncaught exception "Exception" thrown: I take exception to this action! in __FILE__ on line __LINE__', + 'Error: Uncaught "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__', + 'Error: Uncaught "Exception" thrown: I take exception to this action! in __FILE__ on line __LINE__', ), ), 'multiple_non_errors' => array( From 0d580b856d3c741f6113668eb36dd579213ca32a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 3 Nov 2025 09:15:06 -0800 Subject: [PATCH 21/22] Break up sprintf() into multiple lines --- src/wp-includes/template.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index a8ee4cbf45033..7036bb00473df 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -1097,7 +1097,15 @@ static function ( int $level, string $message, ?string $file = null, ?int $line // Adapted from PHP internals: . $format = "%s\n%s: %s in %s on line %s\n%s"; } - $filtered_output .= sprintf( $format, ini_get( 'error_prepend_string' ), $type, $error['message'], $error['file'], $error['line'], ini_get( 'error_append_string' ) ); + $filtered_output .= sprintf( + $format, + ini_get( 'error_prepend_string' ), + $type, + $error['message'], + $error['file'], + $error['line'], + ini_get( 'error_append_string' ) + ); } ini_set( 'display_errors', $original_display_errors ); From a8e7029e5b441986e04495ffcaeb7f6bca4ecd18 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 3 Nov 2025 09:22:12 -0800 Subject: [PATCH 22/22] Move list of config options to constant --- tests/phpunit/tests/template.php | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 2ff08518b4788..e954f68b21923 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -78,6 +78,20 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { */ protected $original_theme_features; + /** + * @var array + */ + const RESTORED_CONFIG_OPTIONS = array( + 'display_errors', + 'error_reporting', + 'log_errors', + 'error_log', + 'default_mimetype', + 'html_errors', + 'error_prepend_string', + 'error_append_string', + ); + /** * @var array */ @@ -117,8 +131,8 @@ public function set_up() { wp_styles(); $this->original_theme_features = $GLOBALS['_wp_theme_features']; - foreach ( array( 'display_errors', 'error_reporting', 'log_errors', 'error_log', 'default_mimetype', 'html_errors', 'error_prepend_string', 'error_append_string' ) as $config ) { - $this->original_ini_config[ $config ] = ini_get( $config ); + foreach ( self::RESTORED_CONFIG_OPTIONS as $option ) { + $this->original_ini_config[ $option ] = ini_get( $option ); } } @@ -128,8 +142,8 @@ public function tear_down() { $wp_styles = $this->original_wp_styles; $GLOBALS['_wp_theme_features'] = $this->original_theme_features; - foreach ( $this->original_ini_config as $config => $value ) { - ini_set( $config, $value ); + foreach ( $this->original_ini_config as $option => $value ) { + ini_set( $option, $value ); } unregister_post_type( 'cpt' );