<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[61120] trunk: General: Ensure errors can be displayed when triggered during finalization of the template enhancement output buffer.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/61120">61120</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/61120","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>westonruter</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2025-11-04 05:26:40 +0000 (Tue, 04 Nov 2025)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>General: Ensure errors can be displayed when triggered during finalization of the template enhancement output buffer.

When `display_errors` (`WP_DEBUG_DISPLAY`) is enabled, errors (including notices, warnings, and deprecations) that are triggered during the `wp_template_enhancement_output_buffer` filter or the `wp_finalized_template_enhancement_output_buffer` action have not been displayed on the frontend since they are emitted in an output buffer callback. Furthermore, as of PHP 8.5 attempting to print anything in an output buffer callback causes a deprecation notice. This introduces an error handler and try/catch block to capture any errors and exceptions that occur during these hooks. If `display_errors` is enabled, these captured errors are then appended to the output buffer so they are visible on the frontend, using the same internal format PHP uses for printing errors. Any exceptions or user errors are converted to warnings so that the template enhancement buffer is not prevented from being flushed.

Developed in https://github.com/WordPress/wordpress-develop/pull/10310

Follow-up to <a href="https://core.trac.wordpress.org/changeset/61111">[61111]</a>, <a href="https://core.trac.wordpress.org/changeset/61088">[61088]</a>, <a href="https://core.trac.wordpress.org/changeset/60936">[60936]</a>.

Props westonruter, dmsnell.
See <a href="https://core.trac.wordpress.org/ticket/43258">#43258</a>, <a href="https://core.trac.wordpress.org/ticket/64126">#64126</a>.
Fixes <a href="https://core.trac.wordpress.org/ticket/64108">#64108</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludestemplatephp">trunk/src/wp-includes/template.php</a></li>
<li><a href="#trunktestsphpunitteststemplatephp">trunk/tests/phpunit/tests/template.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludestemplatephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/template.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/template.php        2025-11-04 00:45:02 UTC (rev 61119)
+++ trunk/src/wp-includes/template.php  2025-11-04 05:26:40 UTC (rev 61120)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -965,50 +965,153 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        $filtered_output = $output;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        /**
-        * 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.
-        *
-        * 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
-        *
-        * @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 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $did_just_catch = false;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        /**
-        * 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
-        * {@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
-        * {@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."
-        *
-        * @since 6.9.0
-        *
-        * @param string $output Finalized output buffer.
-        */
-       do_action( 'wp_finalized_template_enhancement_output_buffer', $filtered_output );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $error_log = array();
+       set_error_handler(
+               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 );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        // Display a caught exception as an error since it prevents any of the output buffer filters from applying.
+                       if ( $did_just_catch ) { // @phpstan-ignore if.alwaysFalse (The variable is set in the catch block below.)
+                               $level = E_USER_ERROR;
+                       }
+
+                       // 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' );
+                       }
+                       return false;
+               }
+       );
+       $original_display_errors = ini_get( 'display_errors' );
+       if ( $original_display_errors ) {
+               ini_set( 'display_errors', 0 );
+       }
+
+       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.
+                *
+                * 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
+                *
+                * @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 ( Throwable $throwable ) {
+               // Emit to the error log as a warning not as an error to prevent halting execution.
+               $did_just_catch = true;
+               trigger_error(
+                       sprintf(
+                               /* translators: %s is the throwable class name */
+                               __( 'Uncaught "%s" thrown:' ),
+                               get_class( $throwable )
+                       ) . ' ' . $throwable->getMessage(),
+                       E_USER_WARNING
+               );
+               $did_just_catch = false;
+       }
+
+       try {
+               /**
+                * 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; 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
+                * {@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."
+                *
+                * @since 6.9.0
+                *
+                * @param string $output Finalized output buffer.
+                */
+               do_action( 'wp_finalized_template_enhancement_output_buffer', $filtered_output );
+       } catch ( Throwable $throwable ) {
+               // Emit to the error log as a warning not as an error to prevent halting execution.
+               $did_just_catch = true;
+               trigger_error(
+                       sprintf(
+                               /* translators: %s is the class name */
+                               __( 'Uncaught "%s" thrown:' ),
+                               get_class( $throwable )
+                       ) . ' ' . $throwable->getMessage(),
+                       E_USER_WARNING
+               );
+               $did_just_catch = false;
+       }
+
+       // Append any errors to be displayed before returning flushing the buffer.
+       if ( $original_display_errors && 'stderr' !== $original_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';
+                       }
+
+                       if ( ini_get( 'html_errors' ) ) {
+                               /*
+                                * Adapted from PHP internals: <https://github.com/php/php-src/blob/a979e9f897a90a580e883b1f39ce5673686ffc67/main/main.c#L1478>.
+                                * The self-closing tags are a vestige of the XHTML past!
+                                */
+                               $format = "%s<br />\n<b>%s</b>:  %s in <b>%s</b> on line <b>%s</b><br />\n%s";
+                       } else {
+                               // Adapted from PHP internals: <https://github.com/php/php-src/blob/a979e9f897a90a580e883b1f39ce5673686ffc67/main/main.c#L1492>.
+                               $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' )
+                       );
+               }
+
+               ini_set( 'display_errors', $original_display_errors );
+       }
+
+       restore_error_handler();
+
</ins><span class="cx" style="display: block; padding: 0 10px">         return $filtered_output;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunktestsphpunitteststemplatephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/template.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/template.php    2025-11-04 00:45:02 UTC (rev 61119)
+++ trunk/tests/phpunit/tests/template.php      2025-11-04 05:26:40 UTC (rev 61120)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -64,11 +64,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @var string
-        */
-       protected $original_default_mimetype;
-
-       /**
</del><span class="cx" style="display: block; padding: 0 10px">          * @var WP_Scripts|null
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        protected $original_wp_scripts;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -83,9 +78,28 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        protected $original_theme_features;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /**
+        * @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
+        */
+       protected $original_ini_config;
+
</ins><span class="cx" style="display: block; padding: 0 10px">         public function set_up() {
</span><span class="cx" style="display: block; padding: 0 10px">                parent::set_up();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->original_default_mimetype = ini_get( 'default_mimetype' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px">                 register_post_type(
</span><span class="cx" style="display: block; padding: 0 10px">                        'cpt',
</span><span class="cx" style="display: block; padding: 0 10px">                        array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -117,6 +131,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                wp_styles();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->original_theme_features = $GLOBALS['_wp_theme_features'];
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                foreach ( self::RESTORED_CONFIG_OPTIONS as $option ) {
+                       $this->original_ini_config[ $option ] = ini_get( $option );
+               }
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        public function tear_down() {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -125,8 +142,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_styles  = $this->original_wp_styles;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $GLOBALS['_wp_theme_features'] = $this->original_theme_features;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                foreach ( $this->original_ini_config as $option => $value ) {
+                       ini_set( $option, $value );
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                ini_set( 'default_mimetype', $this->original_default_mimetype );
</del><span class="cx" style="display: block; padding: 0 10px">                 unregister_post_type( 'cpt' );
</span><span class="cx" style="display: block; padding: 0 10px">                unregister_taxonomy( 'taxo' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->set_permalink_structure( '' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -978,6 +997,380 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * 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_processing(): 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_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 during filter. in __FILE__ on line __LINE__',
+                                       'PHP Deprecated:  You are history during action. in __FILE__ on line __LINE__',
+                               ),
+                               'expected_displayed_errors' => array(
+                                       '<b>Deprecated</b>:  You are history during filter. in <b>__FILE__</b> on line <b>__LINE__</b>',
+                                       '<b>Deprecated</b>:  You are history during action. in <b>__FILE__</b> on line <b>__LINE__</b>',
+                               ),
+                       ),
+                       'notice'                                  => array(
+                               'ini_config_options'        => $log_and_display_all,
+                               '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 during filter. in __FILE__ on line __LINE__',
+                                       'PHP Notice:  POSTED: No trespassing during action. in __FILE__ on line __LINE__',
+                               ),
+                               'expected_displayed_errors' => array(
+                                       '<b>Notice</b>:  POSTED: No trespassing during filter. in <b>__FILE__</b> on line <b>__LINE__</b>',
+                                       '<b>Notice</b>:  POSTED: No trespassing during action. in <b>__FILE__</b> on line <b>__LINE__</b>',
+                               ),
+                       ),
+                       'warning'                                 => array(
+                               'ini_config_options'        => $log_and_display_all,
+                               '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 durante filtro. in __FILE__ on line __LINE__',
+                                       'PHP Warning:  AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__',
+                               ),
+                               'expected_displayed_errors' => array(
+                                       '<b>Warning</b>:  AVISO: Piso mojado durante filtro. in <b>__FILE__</b> on line <b>__LINE__</b>',
+                                       '<b>Warning</b>:  AVISO: Piso mojado durante acción. in <b>__FILE__</b> on line <b>__LINE__</b>',
+                               ),
+                       ),
+                       '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
+                               },
+                               '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" 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(
+                                       '<b>Error</b>:  Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in <b>__FILE__</b> on line <b>__LINE__</b>',
+                                       '<b>Error</b>:  Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in <b>__FILE__</b> on line <b>__LINE__</b>',
+                               ),
+                       ),
+                       'exception'                               => array(
+                               'ini_config_options'        => $log_and_display_all,
+                               '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" 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(
+                                       '<b>Error</b>:  Uncaught "Exception" thrown: I take exception to this filter! in <b>__FILE__</b> on line <b>__LINE__</b>',
+                                       '<b>Error</b>:  Uncaught "Exception" thrown: I take exception to this action! in <b>__FILE__</b> on line <b>__LINE__</b>',
+                               ),
+                       ),
+                       '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 );
+                                       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 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(
+                                       '<b>Deprecated</b>:  You are history during filter. in <b>__FILE__</b> on line <b>__LINE__</b>',
+                                       '<b>Notice</b>:  POSTED: No trespassing during filter. in <b>__FILE__</b> on line <b>__LINE__</b>',
+                                       '<b>Warning</b>:  AVISO: Piso mojado durante filtro. in <b>__FILE__</b> on line <b>__LINE__</b>',
+                                       '<b>Deprecated</b>:  You are history during action. in <b>__FILE__</b> on line <b>__LINE__</b>',
+                                       '<b>Notice</b>:  POSTED: No trespassing during action. in <b>__FILE__</b> on line <b>__LINE__</b>',
+                                       '<b>Warning</b>:  AVISO: Piso mojado durante acción. in <b>__FILE__</b> on line <b>__LINE__</b>',
+                               ),
+                       ),
+                       'deprecated_without_html'                 => array(
+                               'ini_config_options'        => array_merge(
+                                       $log_and_display_all,
+                                       array(
+                                               'html_errors' => false,
+                                       )
+                               ),
+                               '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 during filter. in __FILE__ on line __LINE__',
+                               ),
+                               'expected_displayed_errors' => array(
+                                       'Deprecated: You are history during filter. in __FILE__ on line __LINE__',
+                               ),
+                       ),
+                       'warning_in_eval_with_prepend_and_append' => array(
+                               'ini_config_options'        => array_merge(
+                                       $log_and_display_all,
+                                       array(
+                                               'error_prepend_string' => '<details><summary>PHP Problem!</summary>',
+                                               'error_append_string'  => '</details>',
+                                       )
+                               ),
+                               '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 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(
+                                       '<b>Warning</b>:  AVISO: Piso mojado durante filtro. in <b>__FILE__ : eval()\'d code</b> on line <b>__LINE__</b>',
+                                       '<b>Warning</b>:  AVISO: Piso mojado durante acción. in <b>__FILE__ : eval()\'d code</b> on line <b>__LINE__</b>',
+                               ),
+                       ),
+                       '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();
+               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 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_processing
+        */
+       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();
+
+               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_filter_errors ): string {
+                               $buffer = str_replace( 'Hello', 'Goodbye', $buffer );
+                               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.' );
+
+               ?>
+               <!DOCTYPE html>
+               <html lang="en">
+               <head>
+                       <title>Greeting</title>
+               </head>
+               <body>
+                       <h1>Hello World!</h1>
+               </body>
+               </html>
+               <?php
+
+               ob_end_flush(); // End the buffer started by wp_start_template_enhancement_output_buffer().
+
+               $processed_output = ob_get_clean(); // Obtain the output via the wrapper output buffer.
+
+               if ( $expected_processed ) {
+                       $this->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( '<br />', '', $displayed_error );
+                                       $displayed_error = preg_replace(
+                                               '#( in (?:<b>)?).+?' . preg_quote( basename( __FILE__ ), '#' ) . '(\(\d+\))?#',
+                                               '$1__FILE__',
+                                               $displayed_error
+                                       );
+                                       return preg_replace(
+                                               '#( on line (?:<b>)?)\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( '</html>', rtrim( $processed_output ), 'Expected the output to have the error displayed.' );
+               } else {
+                       $this->assertStringEndsWith( '</html>', rtrim( $processed_output ), 'Expected the output to not have the error displayed.' );
+               }
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Tests that wp_load_classic_theme_block_styles_on_demand() does not add hooks for block themes.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 64099
</span></span></pre>
</div>
</div>

</body>
</html>