Make WordPress Core


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