Make WordPress Core


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