Make WordPress Core

Changeset 61120


Ignore:
Timestamp:
11/04/2025 05:26:40 AM (6 weeks ago)
Author:
westonruter
Message:

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 [61111], [61088], [60936].

Props westonruter, dmsnell.
See #43258, #64126.
Fixes #64108.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/template.php

    r61111 r61120  
    966966    $filtered_output = $output;
    967967
    968     /**
    969      * Filters the template enhancement output buffer prior to sending to the client.
    970      *
    971      * This filter only applies the HTML output of an included template. This filter is a progressive enhancement
    972      * intended for applications such as optimizing markup to improve frontend page load performance. Sites must not
    973      * depend on this filter applying since they may opt to stream the responses instead. Callbacks for this filter are
    974      * highly discouraged from using regular expressions to do any kind of replacement on the output. Use the HTML API
    975      * (either `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use {@see DOM\HtmlDocument} as of PHP 8.4 which
    976      * fully supports HTML5.
    977      *
    978      * Important: Because this filter is applied inside an output buffer callback (i.e. display handler), any callbacks
    979      * added to the filter must not attempt to start their own output buffers. Otherwise, PHP will raise a fatal error:
    980      * "Cannot use output buffering in output buffering display handlers."
    981      *
    982      * @since 6.9.0
    983      *
    984      * @param string $filtered_output HTML template enhancement output buffer.
    985      * @param string $output          Original HTML template output buffer.
    986      */
    987     $filtered_output = (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output );
    988 
    989     /**
    990      * Fires after the template enhancement output buffer has been finalized.
    991      *
    992      * This happens immediately before the template enhancement output buffer is flushed. No output may be printed at
    993      * this action. However, HTTP headers may be sent, which makes this action complimentary to the
    994      * {@see 'send_headers'} action, in which headers may be sent before the template has started rendering. In
    995      * contrast, this `wp_finalized_template_enhancement_output_buffer` action is the possible point at which HTTP
    996      * headers can be sent. This action does not fire if the "template enhancement output buffer" was not started. This
    997      * output buffer is automatically started if this action is added before
    998      * {@see wp_start_template_enhancement_output_buffer()} runs at the {@see 'wp_before_include_template'} action with
    999      * priority 1000. Before this point, the output buffer will also be started automatically if there was a
    1000      * {@see 'wp_template_enhancement_output_buffer'} filter added, or if the
    1001      * {@see 'wp_should_output_buffer_template_for_enhancement'} filter is made to return `true`.
    1002      *
    1003      * Important: Because this action fires inside an output buffer callback (i.e. display handler), any callbacks added
    1004      * to the action must not attempt to start their own output buffers. Otherwise, PHP will raise a fatal error:
    1005      * "Cannot use output buffering in output buffering display handlers."
    1006      *
    1007      * @since 6.9.0
    1008      *
    1009      * @param string $output Finalized output buffer.
    1010      */
    1011     do_action( 'wp_finalized_template_enhancement_output_buffer', $filtered_output );
     968    $did_just_catch = false;
     969
     970    $error_log = array();
     971    set_error_handler(
     972        static function ( int $level, string $message, ?string $file = null, ?int $line = null ) use ( &$error_log, &$did_just_catch ) {
     973            // Switch a user error to an exception so that it can be caught and the buffer can be returned.
     974            if ( E_USER_ERROR === $level ) {
     975                throw new Exception( __( 'User error triggered:' ) . ' ' . $message );
     976            }
     977
     978            // Display a caught exception as an error since it prevents any of the output buffer filters from applying.
     979            if ( $did_just_catch ) { // @phpstan-ignore if.alwaysFalse (The variable is set in the catch block below.)
     980                $level = E_USER_ERROR;
     981            }
     982
     983            // Capture a reported error to be displayed by appending to the processed output buffer if display_errors is enabled.
     984            if ( error_reporting() & $level ) {
     985                $error_log[] = compact( 'level', 'message', 'file', 'line' );
     986            }
     987            return false;
     988        }
     989    );
     990    $original_display_errors = ini_get( 'display_errors' );
     991    if ( $original_display_errors ) {
     992        ini_set( 'display_errors', 0 );
     993    }
     994
     995    try {
     996        /**
     997         * Filters the template enhancement output buffer prior to sending to the client.
     998         *
     999         * This filter only applies the HTML output of an included template. This filter is a progressive enhancement
     1000         * intended for applications such as optimizing markup to improve frontend page load performance. Sites must not
     1001         * depend on this filter applying since they may opt to stream the responses instead. Callbacks for this filter
     1002         * are highly discouraged from using regular expressions to do any kind of replacement on the output. Use the
     1003         * HTML API (either `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use {@see DOM\HtmlDocument} as of
     1004         * PHP 8.4 which fully supports HTML5.
     1005         *
     1006         * Do not print any output during this filter. While filters normally don't print anything, this is especially
     1007         * important since this applies during an output buffer callback. Prior to PHP 8.5, the output will be silently
     1008         * omitted, whereas afterward a deprecation notice will be emitted.
     1009         *
     1010         * Important: Because this filter is applied inside an output buffer callback (i.e. display handler), any
     1011         * callbacks added to the filter must not attempt to start their own output buffers. Otherwise, PHP will raise a
     1012         * fatal error: "Cannot use output buffering in output buffering display handlers."
     1013         *
     1014         * @since 6.9.0
     1015         *
     1016         * @param string $filtered_output HTML template enhancement output buffer.
     1017         * @param string $output          Original HTML template output buffer.
     1018         */
     1019        $filtered_output = (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output );
     1020    } catch ( Throwable $throwable ) {
     1021        // Emit to the error log as a warning not as an error to prevent halting execution.
     1022        $did_just_catch = true;
     1023        trigger_error(
     1024            sprintf(
     1025                /* translators: %s is the throwable class name */
     1026                __( 'Uncaught "%s" thrown:' ),
     1027                get_class( $throwable )
     1028            ) . ' ' . $throwable->getMessage(),
     1029            E_USER_WARNING
     1030        );
     1031        $did_just_catch = false;
     1032    }
     1033
     1034    try {
     1035        /**
     1036         * Fires after the template enhancement output buffer has been finalized.
     1037         *
     1038         * This happens immediately before the template enhancement output buffer is flushed. No output may be printed
     1039         * at this action; prior to PHP 8.5, the output will be silently omitted, whereas afterward a deprecation notice
     1040         * will be emitted. Nevertheless, HTTP headers may be sent, which makes this action complimentary to the
     1041         * {@see 'send_headers'} action, in which headers may be sent before the template has started rendering. In
     1042         * contrast, this `wp_finalized_template_enhancement_output_buffer` action is the possible point at which HTTP
     1043         * headers can be sent. This action does not fire if the "template enhancement output buffer" was not started.
     1044         * This output buffer is automatically started if this action is added before
     1045         * {@see wp_start_template_enhancement_output_buffer()} runs at the {@see 'wp_before_include_template'} action
     1046         * with priority 1000. Before this point, the output buffer will also be started automatically if there was a
     1047         * {@see 'wp_template_enhancement_output_buffer'} filter added, or if the
     1048         * {@see 'wp_should_output_buffer_template_for_enhancement'} filter is made to return `true`.
     1049         *
     1050         * Important: Because this action fires inside an output buffer callback (i.e. display handler), any callbacks
     1051         * added to the action must not attempt to start their own output buffers. Otherwise, PHP will raise a fatal
     1052         * error: "Cannot use output buffering in output buffering display handlers."
     1053         *
     1054         * @since 6.9.0
     1055         *
     1056         * @param string $output Finalized output buffer.
     1057         */
     1058        do_action( 'wp_finalized_template_enhancement_output_buffer', $filtered_output );
     1059    } catch ( Throwable $throwable ) {
     1060        // Emit to the error log as a warning not as an error to prevent halting execution.
     1061        $did_just_catch = true;
     1062        trigger_error(
     1063            sprintf(
     1064                /* translators: %s is the class name */
     1065                __( 'Uncaught "%s" thrown:' ),
     1066                get_class( $throwable )
     1067            ) . ' ' . $throwable->getMessage(),
     1068            E_USER_WARNING
     1069        );
     1070        $did_just_catch = false;
     1071    }
     1072
     1073    // Append any errors to be displayed before returning flushing the buffer.
     1074    if ( $original_display_errors && 'stderr' !== $original_display_errors ) {
     1075        foreach ( $error_log as $error ) {
     1076            switch ( $error['level'] ) {
     1077                case E_USER_NOTICE:
     1078                    $type = 'Notice';
     1079                    break;
     1080                case E_USER_DEPRECATED:
     1081                    $type = 'Deprecated';
     1082                    break;
     1083                case E_USER_WARNING:
     1084                    $type = 'Warning';
     1085                    break;
     1086                default:
     1087                    $type = 'Error';
     1088            }
     1089
     1090            if ( ini_get( 'html_errors' ) ) {
     1091                /*
     1092                 * Adapted from PHP internals: <https://github.com/php/php-src/blob/a979e9f897a90a580e883b1f39ce5673686ffc67/main/main.c#L1478>.
     1093                 * The self-closing tags are a vestige of the XHTML past!
     1094                 */
     1095                $format = "%s<br />\n<b>%s</b>:  %s in <b>%s</b> on line <b>%s</b><br />\n%s";
     1096            } else {
     1097                // Adapted from PHP internals: <https://github.com/php/php-src/blob/a979e9f897a90a580e883b1f39ce5673686ffc67/main/main.c#L1492>.
     1098                $format = "%s\n%s: %s in %s on line %s\n%s";
     1099            }
     1100            $filtered_output .= sprintf(
     1101                $format,
     1102                ini_get( 'error_prepend_string' ),
     1103                $type,
     1104                $error['message'],
     1105                $error['file'],
     1106                $error['line'],
     1107                ini_get( 'error_append_string' )
     1108            );
     1109        }
     1110
     1111        ini_set( 'display_errors', $original_display_errors );
     1112    }
     1113
     1114    restore_error_handler();
    10121115
    10131116    return $filtered_output;
  • trunk/tests/phpunit/tests/template.php

    r61111 r61120  
    6565
    6666    /**
    67      * @var string
    68      */
    69     protected $original_default_mimetype;
    70 
    71     /**
    7267     * @var WP_Scripts|null
    7368     */
     
    8378     */
    8479    protected $original_theme_features;
     80
     81    /**
     82     * @var array
     83     */
     84    const RESTORED_CONFIG_OPTIONS = array(
     85        'display_errors',
     86        'error_reporting',
     87        'log_errors',
     88        'error_log',
     89        'default_mimetype',
     90        'html_errors',
     91        'error_prepend_string',
     92        'error_append_string',
     93    );
     94
     95    /**
     96     * @var array
     97     */
     98    protected $original_ini_config;
    8599
    86100    public function set_up() {
    87101        parent::set_up();
    88         $this->original_default_mimetype = ini_get( 'default_mimetype' );
     102
    89103        register_post_type(
    90104            'cpt',
     
    118132
    119133        $this->original_theme_features = $GLOBALS['_wp_theme_features'];
     134        foreach ( self::RESTORED_CONFIG_OPTIONS as $option ) {
     135            $this->original_ini_config[ $option ] = ini_get( $option );
     136        }
    120137    }
    121138
     
    126143
    127144        $GLOBALS['_wp_theme_features'] = $this->original_theme_features;
    128 
    129         ini_set( 'default_mimetype', $this->original_default_mimetype );
     145        foreach ( $this->original_ini_config as $option => $value ) {
     146            ini_set( $option, $value );
     147        }
     148
    130149        unregister_post_type( 'cpt' );
    131150        unregister_taxonomy( 'taxo' );
     
    979998
    980999    /**
     1000     * Data provider for data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_processing.
     1001     *
     1002     * @return array
     1003     */
     1004    public function data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_processing(): array {
     1005        $log_and_display_all = array(
     1006            'error_reporting' => E_ALL,
     1007            'display_errors'  => true,
     1008            'log_errors'      => true,
     1009            'html_errors'     => true,
     1010        );
     1011
     1012        $tests = array(
     1013            'deprecated'                              => array(
     1014                'ini_config_options'        => $log_and_display_all,
     1015                'emit_filter_errors'        => static function () {
     1016                    trigger_error( 'You are history during filter.', E_USER_DEPRECATED );
     1017                },
     1018                'emit_action_errors'        => static function () {
     1019                    trigger_error( 'You are history during action.', E_USER_DEPRECATED );
     1020                },
     1021                'expected_processed'        => true,
     1022                'expected_error_log'        => array(
     1023                    'PHP Deprecated:  You are history during filter. in __FILE__ on line __LINE__',
     1024                    'PHP Deprecated:  You are history during action. in __FILE__ on line __LINE__',
     1025                ),
     1026                'expected_displayed_errors' => array(
     1027                    '<b>Deprecated</b>:  You are history during filter. in <b>__FILE__</b> on line <b>__LINE__</b>',
     1028                    '<b>Deprecated</b>:  You are history during action. in <b>__FILE__</b> on line <b>__LINE__</b>',
     1029                ),
     1030            ),
     1031            'notice'                                  => array(
     1032                'ini_config_options'        => $log_and_display_all,
     1033                'emit_filter_errors'        => static function () {
     1034                    trigger_error( 'POSTED: No trespassing during filter.', E_USER_NOTICE );
     1035                },
     1036                'emit_action_errors'        => static function () {
     1037                    trigger_error( 'POSTED: No trespassing during action.', E_USER_NOTICE );
     1038                },
     1039                'expected_processed'        => true,
     1040                'expected_error_log'        => array(
     1041                    'PHP Notice:  POSTED: No trespassing during filter. in __FILE__ on line __LINE__',
     1042                    'PHP Notice:  POSTED: No trespassing during action. in __FILE__ on line __LINE__',
     1043                ),
     1044                'expected_displayed_errors' => array(
     1045                    '<b>Notice</b>:  POSTED: No trespassing during filter. in <b>__FILE__</b> on line <b>__LINE__</b>',
     1046                    '<b>Notice</b>:  POSTED: No trespassing during action. in <b>__FILE__</b> on line <b>__LINE__</b>',
     1047                ),
     1048            ),
     1049            'warning'                                 => array(
     1050                'ini_config_options'        => $log_and_display_all,
     1051                'emit_filter_errors'        => static function () {
     1052                    trigger_error( 'AVISO: Piso mojado durante filtro.', E_USER_WARNING );
     1053                },
     1054                'emit_action_errors'        => static function () {
     1055                    trigger_error( 'AVISO: Piso mojado durante acción.', E_USER_WARNING );
     1056                },
     1057                'expected_processed'        => true,
     1058                'expected_error_log'        => array(
     1059                    'PHP Warning:  AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__',
     1060                    'PHP Warning:  AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__',
     1061                ),
     1062                'expected_displayed_errors' => array(
     1063                    '<b>Warning</b>:  AVISO: Piso mojado durante filtro. in <b>__FILE__</b> on line <b>__LINE__</b>',
     1064                    '<b>Warning</b>:  AVISO: Piso mojado durante acción. in <b>__FILE__</b> on line <b>__LINE__</b>',
     1065                ),
     1066            ),
     1067            'error'                                   => array(
     1068                'ini_config_options'        => $log_and_display_all,
     1069                'emit_filter_errors'        => static function () {
     1070                    @trigger_error( 'ERROR: Can this mistake be rectified during filter?', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
     1071                },
     1072                'emit_action_errors'        => static function () {
     1073                    @trigger_error( 'ERROR: Can this mistake be rectified during action?', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
     1074                },
     1075                'expected_processed'        => false,
     1076                'expected_error_log'        => array(
     1077                    'PHP Warning:  Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__',
     1078                    'PHP Warning:  Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__',
     1079                ),
     1080                'expected_displayed_errors' => array(
     1081                    '<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>',
     1082                    '<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>',
     1083                ),
     1084            ),
     1085            'exception'                               => array(
     1086                'ini_config_options'        => $log_and_display_all,
     1087                'emit_filter_errors'        => static function () {
     1088                    throw new Exception( 'I take exception to this filter!' );
     1089                },
     1090                'emit_action_errors'        => static function () {
     1091                    throw new Exception( 'I take exception to this action!' );
     1092                },
     1093                'expected_processed'        => false,
     1094                'expected_error_log'        => array(
     1095                    'PHP Warning:  Uncaught "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__',
     1096                    'PHP Warning:  Uncaught "Exception" thrown: I take exception to this action! in __FILE__ on line __LINE__',
     1097                ),
     1098                'expected_displayed_errors' => array(
     1099                    '<b>Error</b>:  Uncaught "Exception" thrown: I take exception to this filter! in <b>__FILE__</b> on line <b>__LINE__</b>',
     1100                    '<b>Error</b>:  Uncaught "Exception" thrown: I take exception to this action! in <b>__FILE__</b> on line <b>__LINE__</b>',
     1101                ),
     1102            ),
     1103            'multiple_non_errors'                     => array(
     1104                'ini_config_options'        => $log_and_display_all,
     1105                'emit_filter_errors'        => static function () {
     1106                    trigger_error( 'You are history during filter.', E_USER_DEPRECATED );
     1107                    trigger_error( 'POSTED: No trespassing during filter.', E_USER_NOTICE );
     1108                    trigger_error( 'AVISO: Piso mojado durante filtro.', E_USER_WARNING );
     1109                },
     1110                'emit_action_errors'        => static function () {
     1111                    trigger_error( 'You are history during action.', E_USER_DEPRECATED );
     1112                    trigger_error( 'POSTED: No trespassing during action.', E_USER_NOTICE );
     1113                    trigger_error( 'AVISO: Piso mojado durante acción.', E_USER_WARNING );
     1114                },
     1115                'expected_processed'        => true,
     1116                'expected_error_log'        => array(
     1117                    'PHP Deprecated:  You are history during filter. in __FILE__ on line __LINE__',
     1118                    'PHP Notice:  POSTED: No trespassing during filter. in __FILE__ on line __LINE__',
     1119                    'PHP Warning:  AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__',
     1120                    'PHP Deprecated:  You are history during action. in __FILE__ on line __LINE__',
     1121                    'PHP Notice:  POSTED: No trespassing during action. in __FILE__ on line __LINE__',
     1122                    'PHP Warning:  AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__',
     1123                ),
     1124                'expected_displayed_errors' => array(
     1125                    '<b>Deprecated</b>:  You are history during filter. in <b>__FILE__</b> on line <b>__LINE__</b>',
     1126                    '<b>Notice</b>:  POSTED: No trespassing during filter. in <b>__FILE__</b> on line <b>__LINE__</b>',
     1127                    '<b>Warning</b>:  AVISO: Piso mojado durante filtro. in <b>__FILE__</b> on line <b>__LINE__</b>',
     1128                    '<b>Deprecated</b>:  You are history during action. in <b>__FILE__</b> on line <b>__LINE__</b>',
     1129                    '<b>Notice</b>:  POSTED: No trespassing during action. in <b>__FILE__</b> on line <b>__LINE__</b>',
     1130                    '<b>Warning</b>:  AVISO: Piso mojado durante acción. in <b>__FILE__</b> on line <b>__LINE__</b>',
     1131                ),
     1132            ),
     1133            'deprecated_without_html'                 => array(
     1134                'ini_config_options'        => array_merge(
     1135                    $log_and_display_all,
     1136                    array(
     1137                        'html_errors' => false,
     1138                    )
     1139                ),
     1140                'emit_filter_errors'        => static function () {
     1141                    trigger_error( 'You are history during filter.', E_USER_DEPRECATED );
     1142                },
     1143                'emit_action_errors'        => null,
     1144                'expected_processed'        => true,
     1145                'expected_error_log'        => array(
     1146                    'PHP Deprecated:  You are history during filter. in __FILE__ on line __LINE__',
     1147                ),
     1148                'expected_displayed_errors' => array(
     1149                    'Deprecated: You are history during filter. in __FILE__ on line __LINE__',
     1150                ),
     1151            ),
     1152            'warning_in_eval_with_prepend_and_append' => array(
     1153                'ini_config_options'        => array_merge(
     1154                    $log_and_display_all,
     1155                    array(
     1156                        'error_prepend_string' => '<details><summary>PHP Problem!</summary>',
     1157                        'error_append_string'  => '</details>',
     1158                    )
     1159                ),
     1160                'emit_filter_errors'        => static function () {
     1161                    eval( "trigger_error( 'AVISO: Piso mojado durante filtro.', E_USER_WARNING );" ); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- We're in a test!
     1162                },
     1163                'emit_action_errors'        => static function () {
     1164                    eval( "trigger_error( 'AVISO: Piso mojado durante acción.', E_USER_WARNING );" ); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- We're in a test!
     1165                },
     1166                'expected_processed'        => true,
     1167                'expected_error_log'        => array(
     1168                    'PHP Warning:  AVISO: Piso mojado durante filtro. in __FILE__ : eval()\'d code on line __LINE__',
     1169                    'PHP Warning:  AVISO: Piso mojado durante acción. in __FILE__ : eval()\'d code on line __LINE__',
     1170                ),
     1171                'expected_displayed_errors' => array(
     1172                    '<b>Warning</b>:  AVISO: Piso mojado durante filtro. in <b>__FILE__ : eval()\'d code</b> on line <b>__LINE__</b>',
     1173                    '<b>Warning</b>:  AVISO: Piso mojado durante acción. in <b>__FILE__ : eval()\'d code</b> on line <b>__LINE__</b>',
     1174                ),
     1175            ),
     1176            'notice_with_display_errors_stderr'       => array(
     1177                'ini_config_options'        => array_merge(
     1178                    $log_and_display_all,
     1179                    array(
     1180                        'display_errors' => 'stderr',
     1181                    )
     1182                ),
     1183                'emit_filter_errors'        => static function () {
     1184                    trigger_error( 'POSTED: No trespassing during filter.' );
     1185                },
     1186                'emit_action_errors'        => static function () {
     1187                    trigger_error( 'POSTED: No trespassing during action.' );
     1188                },
     1189                'expected_processed'        => true,
     1190                'expected_error_log'        => array(
     1191                    'PHP Notice:  POSTED: No trespassing during filter. in __FILE__ on line __LINE__',
     1192                    'PHP Notice:  POSTED: No trespassing during action. in __FILE__ on line __LINE__',
     1193                ),
     1194                'expected_displayed_errors' => array(),
     1195            ),
     1196        );
     1197
     1198        $tests_error_reporting_warnings_and_above = array();
     1199        foreach ( $tests as $name => $test ) {
     1200            $test['ini_config_options']['error_reporting'] = E_ALL ^ E_USER_NOTICE ^ E_USER_DEPRECATED;
     1201
     1202            $test['expected_error_log'] = array_values(
     1203                array_filter(
     1204                    $test['expected_error_log'],
     1205                    static function ( $log_entry ) {
     1206                        return ! ( str_contains( $log_entry, 'Notice' ) || str_contains( $log_entry, 'Deprecated' ) );
     1207                    }
     1208                )
     1209            );
     1210
     1211            $test['expected_displayed_errors'] = array_values(
     1212                array_filter(
     1213                    $test['expected_displayed_errors'],
     1214                    static function ( $log_entry ) {
     1215                        return ! ( str_contains( $log_entry, 'Notice' ) || str_contains( $log_entry, 'Deprecated' ) );
     1216                    }
     1217                )
     1218            );
     1219
     1220            $tests_error_reporting_warnings_and_above[ "{$name}_with_warnings_and_above_reported" ] = $test;
     1221        }
     1222
     1223        $tests_without_display_errors = array();
     1224        foreach ( $tests as $name => $test ) {
     1225            $test['ini_config_options']['display_errors'] = false;
     1226            $test['expected_displayed_errors']            = array();
     1227
     1228            $tests_without_display_errors[ "{$name}_without_display_errors" ] = $test;
     1229        }
     1230
     1231        $tests_without_display_or_log_errors = array();
     1232        foreach ( $tests as $name => $test ) {
     1233            $test['ini_config_options']['display_errors'] = false;
     1234            $test['ini_config_options']['log_errors']     = false;
     1235            $test['expected_displayed_errors']            = array();
     1236            $test['expected_error_log']                   = array();
     1237
     1238            $tests_without_display_or_log_errors[ "{$name}_without_display_errors_or_log_errors" ] = $test;
     1239        }
     1240
     1241        return array_merge( $tests, $tests_error_reporting_warnings_and_above, $tests_without_display_errors, $tests_without_display_or_log_errors );
     1242    }
     1243
     1244    /**
     1245     * 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.
     1246     *
     1247     * @ticket 43258
     1248     * @ticket 64108
     1249     *
     1250     * @covers ::wp_finalize_template_enhancement_output_buffer
     1251     *
     1252     * @dataProvider data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_processing
     1253     */
     1254    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 {
     1255        // Start a wrapper output buffer so that we can flush the inner buffer.
     1256        ob_start();
     1257
     1258        ini_set( 'error_log', $this->temp_filename() ); // phpcs:ignore WordPress.PHP.IniSet.log_errors_Blacklisted, WordPress.PHP.IniSet.Risky
     1259        foreach ( $ini_config_options as $config => $option ) {
     1260            ini_set( $config, $option );
     1261        }
     1262
     1263        add_filter(
     1264            'wp_template_enhancement_output_buffer',
     1265            static function ( string $buffer ) use ( $emit_filter_errors ): string {
     1266                $buffer = str_replace( 'Hello', 'Goodbye', $buffer );
     1267                if ( $emit_filter_errors ) {
     1268                    $emit_filter_errors();
     1269                }
     1270                return $buffer;
     1271            }
     1272        );
     1273
     1274        if ( $emit_action_errors ) {
     1275            add_action(
     1276                'wp_finalized_template_enhancement_output_buffer',
     1277                static function () use ( $emit_action_errors ): void {
     1278                    $emit_action_errors();
     1279                }
     1280            );
     1281        }
     1282
     1283        $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' );
     1284
     1285        ?>
     1286        <!DOCTYPE html>
     1287        <html lang="en">
     1288        <head>
     1289            <title>Greeting</title>
     1290        </head>
     1291        <body>
     1292            <h1>Hello World!</h1>
     1293        </body>
     1294        </html>
     1295        <?php
     1296
     1297        ob_end_flush(); // End the buffer started by wp_start_template_enhancement_output_buffer().
     1298
     1299        $processed_output = ob_get_clean(); // Obtain the output via the wrapper output buffer.
     1300
     1301        if ( $expected_processed ) {
     1302            $this->assertStringContainsString( 'Goodbye', $processed_output, 'Expected the output buffer to have been processed.' );
     1303        } else {
     1304            $this->assertStringNotContainsString( 'Goodbye', $processed_output, 'Expected the output buffer to not have been processed.' );
     1305        }
     1306
     1307        $actual_error_log = array_values(
     1308            array_map(
     1309                static function ( string $error_log_entry ): string {
     1310                    $error_log_entry = preg_replace(
     1311                        '/^\[.+?] /',
     1312                        '',
     1313                        $error_log_entry
     1314                    );
     1315                    $error_log_entry = preg_replace(
     1316                        '#(?<= in ).+?' . preg_quote( basename( __FILE__ ), '#' ) . '(\(\d+\))?#',
     1317                        '__FILE__',
     1318                        $error_log_entry
     1319                    );
     1320                    return preg_replace(
     1321                        '#(?<= on line )\d+#',
     1322                        '__LINE__',
     1323                        $error_log_entry
     1324                    );
     1325                },
     1326                array_filter( explode( "\n", trim( file_get_contents( ini_get( 'error_log' ) ) ) ) )
     1327            )
     1328        );
     1329
     1330        $this->assertSame(
     1331            $expected_error_log,
     1332            $actual_error_log,
     1333            'Expected same error log entries. Snapshot: ' . var_export( $actual_error_log, true )
     1334        );
     1335
     1336        $displayed_errors = array_values(
     1337            array_map(
     1338                static function ( string $displayed_error ): string {
     1339                    $displayed_error = str_replace( '<br />', '', $displayed_error );
     1340                    $displayed_error = preg_replace(
     1341                        '#( in (?:<b>)?).+?' . preg_quote( basename( __FILE__ ), '#' ) . '(\(\d+\))?#',
     1342                        '$1__FILE__',
     1343                        $displayed_error
     1344                    );
     1345                    return preg_replace(
     1346                        '#( on line (?:<b>)?)\d+#',
     1347                        '$1__LINE__',
     1348                        $displayed_error
     1349                    );
     1350                },
     1351                array_filter(
     1352                    explode( "\n", trim( $processed_output ) ),
     1353                    static function ( $line ): bool {
     1354                        return str_contains( $line, ' in ' );
     1355                    }
     1356                )
     1357            )
     1358        );
     1359
     1360        $this->assertSame(
     1361            $expected_displayed_errors,
     1362            $displayed_errors,
     1363            'Expected the displayed errors to be the same. Snapshot: ' . var_export( $displayed_errors, true )
     1364        );
     1365
     1366        if ( count( $expected_displayed_errors ) > 0 ) {
     1367            $this->assertStringEndsNotWith( '</html>', rtrim( $processed_output ), 'Expected the output to have the error displayed.' );
     1368        } else {
     1369            $this->assertStringEndsWith( '</html>', rtrim( $processed_output ), 'Expected the output to not have the error displayed.' );
     1370        }
     1371    }
     1372
     1373    /**
    9811374     * Tests that wp_load_classic_theme_block_styles_on_demand() does not add hooks for block themes.
    9821375     *
Note: See TracChangeset for help on using the changeset viewer.