Make WordPress Core

Changeset 61120


Ignore:
Timestamp:
11/04/2025 05:26:40 AM (11 months 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.