Make WordPress Core


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

File:
1 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;
Note: See TracChangeset for help on using the changeset viewer.