From 39a3907d634a7924b5a9c0a0190219fb0f199858 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 25 Feb 2025 22:32:10 -0800 Subject: [PATCH 01/32] Add output buffering for the rendered template --- src/wp-includes/template-loader.php | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/wp-includes/template-loader.php b/src/wp-includes/template-loader.php index 0fd08545cc67d..5c91430d4c994 100644 --- a/src/wp-includes/template-loader.php +++ b/src/wp-includes/template-loader.php @@ -1,4 +1,44 @@ . + */ + PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE +); + /** * Loads the correct template based on the visitor's url * From f576c3c3d67f7cbd680bbf66ba634f8471df0864 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 13 Mar 2025 09:32:26 -0700 Subject: [PATCH 02/32] Rework filter to exclusively apply to an HTML output buffer Co-authored-by: Dennis Snell --- src/wp-includes/template-loader.php | 42 ++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/template-loader.php b/src/wp-includes/template-loader.php index 5c91430d4c994..b277947ca913e 100644 --- a/src/wp-includes/template-loader.php +++ b/src/wp-includes/template-loader.php @@ -7,18 +7,36 @@ static function ( string $output, ?int $phase ): string { return $output; } - /** - * Filters the template output buffer prior to sending to the client. - * - * The output buffer is started before the `template_redirect` action is triggered, allowing templates rendered - * at that action to also have their output filtered. - * - * @since n.e.x.t - * - * @param string $output Output buffer. - * @return string Filtered output buffer. - */ - return (string) apply_filters( 'wp_template_output_buffer', $output ); + // Detect if the response is an HTML content type. + $is_html_content_type = false; + $headers_list = array_merge( + array( 'Content-Type: ' . ini_get( 'default_mimetype' ) ), + headers_list() + ); + foreach ( $headers_list as $header ) { + $header_parts = preg_split( '/\s*[:;]\s*/', strtolower( $header ) ); + if ( is_array( $header_parts ) && count( $header_parts ) >= 2 && 'content-type' === $header_parts[0] ) { + $is_html_content_type = in_array( $header_parts[1], array( 'text/html', 'application/xhtml+xml' ), true ); + } + } + // TODO: Also check if str_starts_with( ltrim( $buffer ), '<' )? Or check if str_contains( ' Date: Thu, 13 Mar 2025 09:34:40 -0700 Subject: [PATCH 03/32] Add wp_final_output_buffer action --- src/wp-includes/template-loader.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/wp-includes/template-loader.php b/src/wp-includes/template-loader.php index b277947ca913e..8e7423a7378c9 100644 --- a/src/wp-includes/template-loader.php +++ b/src/wp-includes/template-loader.php @@ -36,6 +36,17 @@ static function ( string $output, ?int $phase ): string { $output = (string) apply_filters( 'wp_output_buffer_html', $output ); } + /** + * Fires after the output buffer has been filtered prior to sending to the client. + * + * This is useful for caching plugins to capture the page output for storage. + * + * @since n.e.x.t + * + * @param string $output Output buffer. + */ + do_action( 'wp_final_output_buffer', $output ); + return $output; }, 0, // Unlimited buffer size so that entire output is passed to the filter. From e9a46aa6508a2f63628dc1abd3f10201bddc6cd5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 22 Sep 2025 16:43:18 -0700 Subject: [PATCH 04/32] Add wp_before_include_template action and move OB handling to new functions --- src/wp-includes/default-filters.php | 1 + src/wp-includes/template-loader.php | 77 +++----------------- src/wp-includes/template.php | 94 +++++++++++++++++++++++++ tests/phpunit/tests/template.php | 104 ++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 68 deletions(-) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 936bbb6a8673f..733783313481c 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -422,6 +422,7 @@ add_action( 'do_all_pings', 'generic_ping', 10, 0 ); add_action( 'do_robots', 'do_robots' ); add_action( 'do_favicon', 'do_favicon' ); +add_action( 'wp_before_include_template', 'wp_start_template_output_buffer', 10, 1 ); add_action( 'set_comment_cookies', 'wp_set_comment_cookies', 10, 3 ); add_action( 'sanitize_comment_cookies', 'sanitize_comment_cookies' ); add_action( 'init', 'smilies_init', 5 ); diff --git a/src/wp-includes/template-loader.php b/src/wp-includes/template-loader.php index 017b989229cf0..f9f349a3ccd7e 100644 --- a/src/wp-includes/template-loader.php +++ b/src/wp-includes/template-loader.php @@ -1,73 +1,5 @@ = 2 && 'content-type' === $header_parts[0] ) { - $is_html_content_type = in_array( $header_parts[1], array( 'text/html', 'application/xhtml+xml' ), true ); - } - } - // TODO: Also check if str_starts_with( ltrim( $buffer ), '<' )? Or check if str_contains( '. - */ - PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE -); - /** * Loads the correct template based on the visitor's URL * @@ -182,6 +114,15 @@ static function ( string $output, ?int $phase ): string { */ $template = apply_filters( 'template_include', $template ); if ( $template ) { + /** + * Fires before including the template. + * + * @since 6.9.0 + * + * @param string $template The path of the template about to be included. + */ + do_action( 'wp_before_include_template', $template ); + include $template; } elseif ( current_user_can( 'switch_themes' ) ) { $theme = wp_get_theme(); diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 401df4db19925..8e392183d0492 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -823,3 +823,97 @@ function load_template( $_template_file, $load_once = true, $args = array() ) { */ do_action( 'wp_after_load_template', $_template_file, $load_once, $args ); } + +/** + * Starts the template output buffer. + * + * This function is called immediately before the template is included. + * + * @since 6.9.0 + * + * @return bool Whether the output buffer successfully started. + */ +function wp_start_template_output_buffer(): bool { + return ob_start( + 'wp_finalize_template_output_buffer', + 0, // Unlimited buffer size so that entire output is passed to the filter. + /* + * Instead of the default PHP_OUTPUT_HANDLER_STDFLAGS (cleanable, flushable, and removable) being used for flags, + * the PHP_OUTPUT_HANDLER_FLUSHABLE flag must be omitted. If the buffer were flushable, then each time that + * ob_flush() is called, it would send a fragment of the output into the output buffer callback. When buffering the + * entire response as an HTML document, this would result in broken HTML processing. + * + * If this ends up being problematic, then PHP_OUTPUT_HANDLER_FLUSHABLE could be added to the $flags and the + * output buffer callback could check if the phase is PHP_OUTPUT_HANDLER_FLUSH and abort any subsequent + * processing while also emitting a _doing_it_wrong(). + * + * The output buffer needs to be removable because WordPress calls wp_ob_end_flush_all() and then calls + * wp_cache_close(). If the buffers are not all flushed before wp_cache_close() is closed, then some output buffer + * handlers (e.g. for caching plugins) may fail to be able to store the page output in the object cache. + * See . + */ + PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE + ); +} + +/** + * Finalizes the output buffer for the included template. + * + * @since 6.9.0 + * + * @see wp_start_template_output_buffer() + * + * @param string $output Output buffer. + * @param int $phase Phase. + * @return string Finalized output buffer. + */ +function wp_finalize_template_output_buffer( string $output, int $phase ): string { + // When the output is being cleaned (e.g. pending template is replaced with error page), do not send it through the filter. + if ( ( $phase & PHP_OUTPUT_HANDLER_CLEAN ) !== 0 ) { + return $output; + } + + // Detect if the response is an HTML content type. + $is_html_content_type = false; + $headers_list = array_merge( + array( 'Content-Type: ' . ini_get( 'default_mimetype' ) ), + headers_list() + ); + foreach ( $headers_list as $header ) { + $header_parts = preg_split( '/\s*[:;]\s*/', strtolower( $header ) ); + if ( is_array( $header_parts ) && count( $header_parts ) >= 2 && 'content-type' === $header_parts[0] ) { + $is_html_content_type = in_array( $header_parts[1], array( 'text/html', 'application/xhtml+xml' ), true ); + } + } + // TODO: Also check if str_starts_with( ltrim( $buffer ), '<' )? Or check if str_contains( 'assertSame( $new_theme->get_stylesheet_directory() . '/index.php', locate_template( $template_names ), 'Incorrect index template found in theme after switch.' ); } + /** + * Tests that wp_start_template_output_buffer() starts the expected output buffer and that the expected hooks fire. + * + * @ticket 43258 + * @covers ::wp_start_template_output_buffer + */ + public function test_wp_start_template_output_buffer(): void { + // Start a wrapper output buffer so that we can flush the inner buffer. + ob_start(); + + $action_args = null; + add_action( + 'wp_final_template_output_buffer', + function () use ( &$action_args ): void { + $action_args = func_get_args(); + }, + 10, + PHP_INT_MAX + ); + + add_filter( + 'wp_template_output_buffer_html', + function ( string $buffer ): string { + $p = WP_HTML_Processor::create_full_parser( $buffer ); + while ( $p->next_tag() ) { + echo $p->get_tag() . PHP_EOL; + switch ( $p->get_tag() ) { + case 'HTML': + $p->set_attribute( 'lang', 'es' ); + break; + case 'TITLE': + $p->set_modifiable_text( 'Saludo' ); + break; + case 'H1': + if ( $p->next_token() && '#text' === $p->get_token_name() ) { + $p->set_modifiable_text( '¡Hola, mundo!' ); + } + break; + } + } + return $p->get_updated_html(); + } + ); + + $initial_ob_level = ob_get_level(); + $this->assertTrue( wp_start_template_output_buffer() ); + $this->assertSame( $initial_ob_level + 1, ob_get_level() ); + + ?> + + + + Greeting + + assertFalse( + @ob_flush(), // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + 'Expected output buffer to not be incrementally flushable.' + ); + ?> + +

Hello World!

+ + + assertSame( 'wp_finalize_template_output_buffer', $ob_status['name'], 'Expected name to be WP function.' ); + $this->assertSame( 1, $ob_status['type'], 'Expected type to be user supplied handler.' ); + $this->assertSame( 0, $ob_status['chunk_size'], 'Expected unlimited chunk size.' ); + + ob_end_flush(); // End the buffer started by wp_start_template_output_buffer(). + $this->assertSame( $initial_ob_level, ob_get_level() ); + $this->assertSame( 1, did_action( 'wp_final_template_output_buffer' ) ); + + // Obtain the output via the wrapper output buffer. + $output = ob_get_clean(); + $this->assertIsString( $output ); + $this->assertIsArray( $action_args ); + $this->assertCount( 2, $action_args ); + $this->assertIsString( $action_args[0] ); + $this->assertStringContainsString( '', $action_args[0] ); + $this->assertStringContainsString( '', $action_args[0] ); + $this->assertStringContainsString( 'Saludo', $action_args[0] ); + $this->assertStringContainsString( '

¡Hola, mundo!

', $action_args[0] ); + $this->assertStringContainsString( '', $action_args[0] ); + $this->assertIsString( $action_args[1] ); + $this->assertStringContainsString( '', $action_args[1] ); + $this->assertStringContainsString( '', $action_args[1] ); + $this->assertStringContainsString( 'Greeting', $action_args[1] ); + $this->assertStringContainsString( '

Hello World!

', $action_args[1] ); + $this->assertStringContainsString( '', $action_args[1] ); + } + + /** + * Tests wp_finalize_template_output_buffer(). + * + * @ticket 43258 + * @covers ::wp_finalize_template_output_buffer + */ + public function test_wp_finalize_template_output_buffer(): void { + $this->markTestIncomplete(); + } + public function assertTemplateHierarchy( $url, array $expected, $message = '' ) { $this->go_to( $url ); $hierarchy = $this->get_template_hierarchy(); From 2c074fa6253d9eb5a9e26622efe8d3f9be7dbc49 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 22 Sep 2025 16:50:48 -0700 Subject: [PATCH 05/32] Add wp_template_output_buffer filter --- src/wp-includes/template.php | 14 ++++++++++++++ tests/phpunit/tests/template.php | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 8e392183d0492..c40a938826468 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -903,6 +903,20 @@ function wp_finalize_template_output_buffer( string $output, int $phase ): strin $filtered_output = (string) apply_filters( 'wp_template_output_buffer_html', $filtered_output ); } + /** + * Filters the template output buffer prior to sending to the client. + * + * This filter applies to HTML and non-HTML template output alike. For example, a "template" could return JSON. Use + * the {@see 'wp_template_output_buffer_html'} filter specifically for filtering HTML output, which is the normal + * case. Notice: It is highly discouraged to use regular expressions to do any kind of replacement on the output. + * + * @since 6.9.0 + * + * @param string $output Output buffer. + * @return string Filtered output buffer. + */ + $filtered_output = (string) apply_filters( 'wp_template_output_buffer', $filtered_output ); + /** * Fires after the output buffer has been filtered prior to sending to the client. * diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 90b0f1462b0f0..53c354e22a418 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -516,7 +516,7 @@ function () use ( &$action_args ): void { add_filter( 'wp_template_output_buffer_html', - function ( string $buffer ): string { + static function ( string $buffer ): string { $p = WP_HTML_Processor::create_full_parser( $buffer ); while ( $p->next_tag() ) { echo $p->get_tag() . PHP_EOL; @@ -538,6 +538,17 @@ function ( string $buffer ): string { } ); + add_filter( + 'wp_template_output_buffer', + function ( string $buffer ): string { + $this->assertStringNotContainsString( 'Hello', $buffer ); + if ( str_starts_with( ltrim( $buffer ), '' ) ) { + $buffer .= "\n\n"; + } + return $buffer; + } + ); + $initial_ob_level = ob_get_level(); $this->assertTrue( wp_start_template_output_buffer() ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); @@ -580,12 +591,14 @@ function ( string $buffer ): string { $this->assertStringContainsString( 'Saludo', $action_args[0] ); $this->assertStringContainsString( '

¡Hola, mundo!

', $action_args[0] ); $this->assertStringContainsString( '', $action_args[0] ); + $this->assertStringContainsString( '', $action_args[0] ); $this->assertIsString( $action_args[1] ); $this->assertStringContainsString( '', $action_args[1] ); $this->assertStringContainsString( '', $action_args[1] ); $this->assertStringContainsString( 'Greeting', $action_args[1] ); $this->assertStringContainsString( '

Hello World!

', $action_args[1] ); $this->assertStringContainsString( '', $action_args[1] ); + $this->assertStringNotContainsString( '', $action_args[1] ); } /** From 7f79ebd1be5d60afa71c5c2afd22458c3df40a34 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 22 Sep 2025 17:04:31 -0700 Subject: [PATCH 06/32] Ensure at least one tag is present for HTML content type --- src/wp-includes/template.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index c40a938826468..303fed39a90ba 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -881,11 +881,21 @@ function wp_finalize_template_output_buffer( string $output, int $phase ): strin ); foreach ( $headers_list as $header ) { $header_parts = preg_split( '/\s*[:;]\s*/', strtolower( $header ) ); - if ( is_array( $header_parts ) && count( $header_parts ) >= 2 && 'content-type' === $header_parts[0] ) { - $is_html_content_type = in_array( $header_parts[1], array( 'text/html', 'application/xhtml+xml' ), true ); + if ( + is_array( $header_parts ) && + count( $header_parts ) >= 2 && + 'content-type' === $header_parts[0] && + in_array( $header_parts[1], array( 'text/html', 'application/xhtml+xml' ), true ) + ) { + $is_html_content_type = true; + break; } } - // TODO: Also check if str_starts_with( ltrim( $buffer ), '<' )? Or check if str_contains( 'next_tag(); + } $filtered_output = $output; if ( $is_html_content_type ) { From 6637ef5d3a221b7bd6083ce68ea510ea66e98186 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 22 Sep 2025 17:16:26 -0700 Subject: [PATCH 07/32] Add test for ending and cleaning an output buffer to avoid processing --- tests/phpunit/tests/template.php | 69 ++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 53c354e22a418..34397057b5f06 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -499,6 +499,7 @@ public function test_locate_template_uses_current_theme() { * * @ticket 43258 * @covers ::wp_start_template_output_buffer + * @covers ::wp_finalize_template_output_buffers */ public function test_wp_start_template_output_buffer(): void { // Start a wrapper output buffer so that we can flush the inner buffer. @@ -601,6 +602,74 @@ function ( string $buffer ): string { $this->assertStringNotContainsString( '', $action_args[1] ); } + /** + * Tests that wp_start_template_output_buffer() starts the expected output buffer and that the expected hooks fire. + * + * @ticket 43258 + * @covers ::wp_start_template_output_buffer + * @covers ::wp_finalize_template_output_buffers + */ + public function test_wp_start_template_output_buffer_cleaned(): void { + // Start a wrapper output buffer so that we can flush the inner buffer. + ob_start(); + + add_filter( + 'wp_template_output_buffer_html', + static function ( string $buffer ): string { + $p = WP_HTML_Processor::create_full_parser( $buffer ); + if ( $p->next_tag( array( 'tag_name' => 'TITLE' ) ) ) { + $p->set_modifiable_text( 'Processed' ); + } + return $p->get_updated_html(); + } + ); + + add_filter( + 'wp_template_output_buffer', + function ( string $buffer ): string { + $buffer .= "\n\n"; + return $buffer; + } + ); + + $initial_ob_level = ob_get_level(); + $this->assertTrue( wp_start_template_output_buffer() ); + $this->assertSame( $initial_ob_level + 1, ob_get_level() ); + + ?> + + + + Unprocessed + + +

Hello World!

+ + + + + + Output Buffer Not Processed + + +

Template rendering aborted!!!

+ + + assertSame( $initial_ob_level, ob_get_level() ); + + $this->assertSame( 0, did_action( 'wp_final_template_output_buffer' ) ); + + // Obtain the output via the wrapper output buffer. + $output = ob_get_clean(); + $this->assertIsString( $output ); + $this->assertStringNotContainsString( 'Unprocessed', $output ); + $this->assertStringNotContainsString( 'Processed', $output ); + $this->assertStringNotContainsString( '', $output ); + $this->assertStringContainsString( 'Output Buffer Not Processed', $output ); + } + /** * Tests wp_finalize_template_output_buffer(). * From b98b81e3d4657dc15696180230f2d031a20e5bad Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 22 Sep 2025 17:34:52 -0700 Subject: [PATCH 08/32] Add test case for JSON --- tests/phpunit/tests/template.php | 120 ++++++++++++++++++++++++++++--- 1 file changed, 109 insertions(+), 11 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 34397057b5f06..aad4771353624 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -63,8 +63,14 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { ); } + /** + * @var string + */ + protected $original_default_mimetype; + public function set_up() { parent::set_up(); + $this->original_default_mimetype = ini_get( 'default_mimetype' ); register_post_type( 'cpt', array( @@ -83,6 +89,7 @@ public function set_up() { } public function tear_down() { + ini_set( 'default_mimetype', $this->original_default_mimetype ); unregister_post_type( 'cpt' ); unregister_taxonomy( 'taxo' ); $this->set_permalink_structure( '' ); @@ -495,13 +502,14 @@ public function test_locate_template_uses_current_theme() { } /** - * Tests that wp_start_template_output_buffer() starts the expected output buffer and that the expected hooks fire. + * Tests that wp_start_template_output_buffer() starts the expected output buffer and that the expected hooks fire for + * an HTML document and that the response is not incrementally flushable. * * @ticket 43258 * @covers ::wp_start_template_output_buffer * @covers ::wp_finalize_template_output_buffers */ - public function test_wp_start_template_output_buffer(): void { + public function test_wp_start_template_output_buffer_for_html(): void { // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); @@ -515,9 +523,12 @@ function () use ( &$action_args ): void { PHP_INT_MAX ); + $called_html_filter = false; add_filter( 'wp_template_output_buffer_html', - static function ( string $buffer ): string { + static function ( string $buffer ) use ( &$called_html_filter ): string { + $called_html_filter = true; + $p = WP_HTML_Processor::create_full_parser( $buffer ); while ( $p->next_tag() ) { echo $p->get_tag() . PHP_EOL; @@ -539,9 +550,11 @@ static function ( string $buffer ): string { } ); + $called_filter = false; add_filter( 'wp_template_output_buffer', - function ( string $buffer ): string { + function ( string $buffer ) use ( &$called_filter ): string { + $called_filter = true; $this->assertStringNotContainsString( 'Hello', $buffer ); if ( str_starts_with( ltrim( $buffer ), '' ) ) { $buffer .= "\n\n"; @@ -579,6 +592,9 @@ function ( string $buffer ): string { ob_end_flush(); // End the buffer started by wp_start_template_output_buffer(). $this->assertSame( $initial_ob_level, ob_get_level() ); + + $this->assertTrue( $called_html_filter ); + $this->assertTrue( $called_filter ); $this->assertSame( 1, did_action( 'wp_final_template_output_buffer' ) ); // Obtain the output via the wrapper output buffer. @@ -603,7 +619,7 @@ function ( string $buffer ): string { } /** - * Tests that wp_start_template_output_buffer() starts the expected output buffer and that the expected hooks fire. + * Tests that wp_start_template_output_buffer() starts the expected output buffer but cleaning prevents any processing. * * @ticket 43258 * @covers ::wp_start_template_output_buffer @@ -613,9 +629,12 @@ public function test_wp_start_template_output_buffer_cleaned(): void { // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); + $called_html_filter = false; add_filter( 'wp_template_output_buffer_html', - static function ( string $buffer ): string { + static function ( string $buffer ) use ( &$called_html_filter ): string { + $called_html_filter = true; + $p = WP_HTML_Processor::create_full_parser( $buffer ); if ( $p->next_tag( array( 'tag_name' => 'TITLE' ) ) ) { $p->set_modifiable_text( 'Processed' ); @@ -624,9 +643,12 @@ static function ( string $buffer ): string { } ); + $called_filter = false; add_filter( 'wp_template_output_buffer', - function ( string $buffer ): string { + function ( string $buffer ) use ( &$called_filter ): string { + $called_filter = true; + $buffer .= "\n\n"; return $buffer; } @@ -659,6 +681,8 @@ function ( string $buffer ): string { $this->assertSame( $initial_ob_level, ob_get_level() ); + $this->assertFalse( $called_html_filter ); + $this->assertFalse( $called_filter ); $this->assertSame( 0, did_action( 'wp_final_template_output_buffer' ) ); // Obtain the output via the wrapper output buffer. @@ -671,13 +695,87 @@ function ( string $buffer ): string { } /** - * Tests wp_finalize_template_output_buffer(). + * Tests that wp_start_template_output_buffer() starts the expected output buffer and that the expected hooks fire for JSON response. * * @ticket 43258 - * @covers ::wp_finalize_template_output_buffer + * @covers ::wp_start_template_output_buffer + * @covers ::wp_finalize_template_output_buffers */ - public function test_wp_finalize_template_output_buffer(): void { - $this->markTestIncomplete(); + public function test_wp_start_template_output_buffer_for_json(): void { + // Start a wrapper output buffer so that we can flush the inner buffer. + ob_start(); + + $action_args = null; + add_action( + 'wp_final_template_output_buffer', + function () use ( &$action_args ): void { + $action_args = func_get_args(); + }, + 10, + PHP_INT_MAX + ); + + $called_html_filter = false; + add_filter( + 'wp_template_output_buffer_html', + static function ( string $buffer ) use ( &$called_html_filter ): string { + $called_html_filter = true; + return $buffer; + } + ); + + $called_filter = false; + add_filter( + 'wp_template_output_buffer', + function ( string $buffer ) use ( &$called_filter ): string { + $called_filter = true; + + $data = json_decode( $buffer, true ); + $data['data']['message'] = '¡Hola, mundo!'; + return json_encode( $data ); + } + ); + + $initial_ob_level = ob_get_level(); + $this->assertTrue( wp_start_template_output_buffer() ); + $this->assertSame( $initial_ob_level + 1, ob_get_level() ); + + ini_set( 'default_mimetype', 'application/json' ); // Since sending a header won't work. + echo wp_json_encode( + array( + 'success' => true, + 'data' => array( + 'message' => 'Hello, world!', + 'fish' => '<', // Something that looks like HTML. + ), + ) + ); + + $ob_status = ob_get_status(); + $this->assertSame( 'wp_finalize_template_output_buffer', $ob_status['name'], 'Expected name to be WP function.' ); + $this->assertSame( 1, $ob_status['type'], 'Expected type to be user supplied handler.' ); + $this->assertSame( 0, $ob_status['chunk_size'], 'Expected unlimited chunk size.' ); + + ob_end_flush(); // End the buffer started by wp_start_template_output_buffer(). + $this->assertSame( $initial_ob_level, ob_get_level() ); + + $this->assertFalse( $called_html_filter ); + $this->assertTrue( $called_filter ); + $this->assertSame( 1, did_action( 'wp_final_template_output_buffer' ) ); + + // Obtain the output via the wrapper output buffer. + $output = ob_get_clean(); + $this->assertIsString( $output ); + $this->assertIsArray( $action_args ); + $this->assertCount( 2, $action_args ); + $this->assertIsString( $action_args[0] ); + $this->assertIsString( $action_args[1] ); + + $filtered_data = json_decode( $action_args[0], true ); + $original_data = json_decode( $action_args[1], true ); + + $this->assertSame( 'Hello, world!', $original_data['data']['message'] ); + $this->assertSame( '¡Hola, mundo!', $filtered_data['data']['message'] ); } public function assertTemplateHierarchy( $url, array $expected, $message = '' ) { From 9f07ad01c5bf65cb3356a65db894d3a726733c50 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 22 Sep 2025 17:41:05 -0700 Subject: [PATCH 09/32] Account for multiple content-type headers --- src/wp-includes/template.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 303fed39a90ba..cfc2ee5d6d869 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -884,11 +884,9 @@ function wp_finalize_template_output_buffer( string $output, int $phase ): strin if ( is_array( $header_parts ) && count( $header_parts ) >= 2 && - 'content-type' === $header_parts[0] && - in_array( $header_parts[1], array( 'text/html', 'application/xhtml+xml' ), true ) + 'content-type' === $header_parts[0] ) { - $is_html_content_type = true; - break; + $is_html_content_type = in_array( $header_parts[1], array( 'text/html', 'application/xhtml+xml' ), true ); } } From 4e7029b71b0294733073138c315b5b459aed1a34 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 22 Sep 2025 17:41:21 -0700 Subject: [PATCH 10/32] Fix passing back the filtered output buffer --- src/wp-includes/template.php | 2 +- tests/phpunit/tests/template.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index cfc2ee5d6d869..6f4ecc6e48df1 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -937,5 +937,5 @@ function wp_finalize_template_output_buffer( string $output, int $phase ): strin */ do_action( 'wp_final_template_output_buffer', $filtered_output, $output ); - return $output; + return $filtered_output; } diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index aad4771353624..e8cba4c95e75f 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -602,7 +602,7 @@ function ( string $buffer ) use ( &$called_filter ): string { $this->assertIsString( $output ); $this->assertIsArray( $action_args ); $this->assertCount( 2, $action_args ); - $this->assertIsString( $action_args[0] ); + $this->assertSame( $action_args[0], $output ); $this->assertStringContainsString( '', $action_args[0] ); $this->assertStringContainsString( '', $action_args[0] ); $this->assertStringContainsString( 'Saludo', $action_args[0] ); @@ -768,7 +768,7 @@ function ( string $buffer ) use ( &$called_filter ): string { $this->assertIsString( $output ); $this->assertIsArray( $action_args ); $this->assertCount( 2, $action_args ); - $this->assertIsString( $action_args[0] ); + $this->assertSame( $action_args[0], $output ); $this->assertIsString( $action_args[1] ); $filtered_data = json_decode( $action_args[0], true ); From fd79c2a10eecb639f0f27936c49aa9a416c2c9fd Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 22 Sep 2025 17:41:50 -0700 Subject: [PATCH 11/32] Remove extra line break --- src/wp-includes/template-loader.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-includes/template-loader.php b/src/wp-includes/template-loader.php index f9f349a3ccd7e..2b9b9c4f90985 100644 --- a/src/wp-includes/template-loader.php +++ b/src/wp-includes/template-loader.php @@ -1,5 +1,4 @@ Date: Fri, 26 Sep 2025 14:39:38 -0700 Subject: [PATCH 12/32] Add test for calling ob_clean() instead of ob_end_clean() --- tests/phpunit/tests/template.php | 86 ++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index e8cba4c95e75f..06f4f8a64dd23 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -619,13 +619,13 @@ function ( string $buffer ) use ( &$called_filter ): string { } /** - * Tests that wp_start_template_output_buffer() starts the expected output buffer but cleaning prevents any processing. + * Tests that wp_start_template_output_buffer() starts the expected output buffer but ending with cleaning prevents any processing. * * @ticket 43258 * @covers ::wp_start_template_output_buffer - * @covers ::wp_finalize_template_output_buffers + * @covers ::wp_finalize_template_output_buffer */ - public function test_wp_start_template_output_buffer_cleaned(): void { + public function test_wp_start_template_output_buffer_ended_cleaned(): void { // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); @@ -694,12 +694,90 @@ function ( string $buffer ) use ( &$called_filter ): string { $this->assertStringContainsString( 'Output Buffer Not Processed', $output ); } + /** + * Tests that wp_start_template_output_buffer() starts the expected output buffer and cleaning allows the template to be replaced. + * + * @ticket 43258 + * @covers ::wp_start_template_output_buffer + * @covers ::wp_finalize_template_output_buffer + */ + public function test_wp_start_template_output_buffer_cleaned_and_replaced(): void { + // Start a wrapper output buffer so that we can flush the inner buffer. + ob_start(); + + $called_html_filter = false; + add_filter( + 'wp_template_output_buffer_html', + static function ( string $buffer ) use ( &$called_html_filter ): string { + $called_html_filter = true; + + $p = WP_HTML_Processor::create_full_parser( $buffer ); + if ( $p->next_tag( array( 'tag_name' => 'TITLE' ) ) ) { + $p->set_modifiable_text( 'Processed' ); + } + return $p->get_updated_html(); + } + ); + + $called_filter = false; + add_filter( + 'wp_template_output_buffer', + function ( string $buffer ) use ( &$called_filter ): string { + $called_filter = true; + + $buffer .= "\n\n"; + return $buffer; + } + ); + + $initial_ob_level = ob_get_level(); + $this->assertTrue( wp_start_template_output_buffer() ); + $this->assertSame( $initial_ob_level + 1, ob_get_level() ); + + ?> + + + + Unprocessed + + +

Hello World!

+ + + + + + Template Replaced + + +

Template Replaced

+

The original template called ob_clean() which allowed this template to take its place.

+ + + assertSame( $initial_ob_level, ob_get_level() ); + + $this->assertTrue( $called_html_filter ); + $this->assertTrue( $called_filter ); + $this->assertSame( 1, did_action( 'wp_final_template_output_buffer' ) ); + + // Obtain the output via the wrapper output buffer. + $output = ob_get_clean(); + $this->assertIsString( $output ); + $this->assertStringNotContainsString( 'Unprocessed', $output ); + $this->assertStringContainsString( 'Processed', $output ); + $this->assertStringContainsString( '', $output ); + $this->assertStringContainsString( '

Template Replaced

', $output ); + } + /** * Tests that wp_start_template_output_buffer() starts the expected output buffer and that the expected hooks fire for JSON response. * * @ticket 43258 * @covers ::wp_start_template_output_buffer - * @covers ::wp_finalize_template_output_buffers + * @covers ::wp_finalize_template_output_buffer */ public function test_wp_start_template_output_buffer_for_json(): void { // Start a wrapper output buffer so that we can flush the inner buffer. From 59cf047555ad7e7fc9e8cb4fc97e89f76aacdff6 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 1 Oct 2025 13:25:20 -0700 Subject: [PATCH 13/32] Include original buffer in param to filters Co-authored-by: Aaron Jorbin --- src/wp-includes/template.php | 14 ++++++---- tests/phpunit/tests/template.php | 47 +++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 6f4ecc6e48df1..9c77d0c40d99b 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -905,10 +905,11 @@ function wp_finalize_template_output_buffer( string $output, int $phase ): strin * * @since 6.9.0 * - * @param string $output Output buffer HTML. + * @param string $filtered_output Filtered output buffer. + * @param string $output Original output buffer. * @return string Filtered output buffer HTML. */ - $filtered_output = (string) apply_filters( 'wp_template_output_buffer_html', $filtered_output ); + $filtered_output = (string) apply_filters( 'wp_template_output_buffer_html', $filtered_output, $output ); } /** @@ -920,10 +921,11 @@ function wp_finalize_template_output_buffer( string $output, int $phase ): strin * * @since 6.9.0 * - * @param string $output Output buffer. + * @param string $filtered_output Filtered output buffer. + * @param string $output Original output buffer. * @return string Filtered output buffer. */ - $filtered_output = (string) apply_filters( 'wp_template_output_buffer', $filtered_output ); + $filtered_output = (string) apply_filters( 'wp_template_output_buffer', $filtered_output, $output ); /** * Fires after the output buffer has been filtered prior to sending to the client. @@ -932,8 +934,8 @@ function wp_finalize_template_output_buffer( string $output, int $phase ): strin * * @since 6.9.0 * - * @param string $filtered_output Output buffer after filters. - * @param string $output Output buffer before filters. + * @param string $filtered_output Filtered output buffer. + * @param string $output Original output buffer. */ do_action( 'wp_final_template_output_buffer', $filtered_output, $output ); diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 06f4f8a64dd23..c511d394cb188 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -523,11 +523,11 @@ function () use ( &$action_args ): void { PHP_INT_MAX ); - $called_html_filter = false; + $html_filter_args = null; add_filter( 'wp_template_output_buffer_html', - static function ( string $buffer ) use ( &$called_html_filter ): string { - $called_html_filter = true; + static function ( string $buffer ) use ( &$html_filter_args ): string { + $html_filter_args = func_get_args(); $p = WP_HTML_Processor::create_full_parser( $buffer ); while ( $p->next_tag() ) { @@ -547,20 +547,24 @@ static function ( string $buffer ) use ( &$called_html_filter ): string { } } return $p->get_updated_html(); - } + }, + 10, + PHP_INT_MAX ); - $called_filter = false; + $filter_args = null; add_filter( 'wp_template_output_buffer', - function ( string $buffer ) use ( &$called_filter ): string { - $called_filter = true; + function ( string $buffer ) use ( &$filter_args ): string { + $filter_args = func_get_args(); $this->assertStringNotContainsString( 'Hello', $buffer ); if ( str_starts_with( ltrim( $buffer ), '' ) ) { $buffer .= "\n\n"; } return $buffer; - } + }, + 10, + PHP_INT_MAX ); $initial_ob_level = ob_get_level(); @@ -593,16 +597,27 @@ function ( string $buffer ) use ( &$called_filter ): string { ob_end_flush(); // End the buffer started by wp_start_template_output_buffer(). $this->assertSame( $initial_ob_level, ob_get_level() ); - $this->assertTrue( $called_html_filter ); - $this->assertTrue( $called_filter ); - $this->assertSame( 1, did_action( 'wp_final_template_output_buffer' ) ); + $this->assertIsArray( $html_filter_args, 'Expected the wp_template_output_buffer_html filter to have applied.' ); + $this->assertCount( 2, $html_filter_args, 'Expected two args to be supplied to the wp_template_output_buffer_html filter.' ); + $this->assertIsString( $html_filter_args[0], 'Expected the $filtered_output param to the wp_template_output_buffer_html filter to be a string.' ); + $this->assertIsString( $html_filter_args[1], 'Expected the $output param to the wp_template_output_buffer_html filter to be a string.' ); + $this->assertSame( $html_filter_args[1], $html_filter_args[0], 'Expected the initial $filtered_output to match $output in the wp_template_output_buffer_html filter.' ); - // Obtain the output via the wrapper output buffer. - $output = ob_get_clean(); + $this->assertIsArray( $filter_args, 'Expected the wp_template_output_buffer filter to have applied.' ); + $this->assertCount( 2, $filter_args, 'Expected two args to be supplied to the wp_template_output_buffer filter.' ); + $this->assertIsString( $filter_args[0], 'Expected the $filtered_output param to the wp_template_output_buffer filter to be a string.' ); + $this->assertIsString( $filter_args[1], 'Expected the $output param to the wp_template_output_buffer filter to be a string.' ); + $this->assertNotSame( $filter_args[1], $filter_args[0], 'Expected the $output and $filtered_output args to not match when applying the wp_template_output_buffer filter since the first latter was modified by a wp_template_output_buffer_html filter.' ); + $this->assertSame( $filter_args[1], $html_filter_args[1], 'Expected the $output param to match between the wp_template_output_buffer and wp_template_output_buffer_html filters.' ); + + $output = ob_get_clean(); // Obtain the output via the wrapper output buffer. $this->assertIsString( $output ); - $this->assertIsArray( $action_args ); - $this->assertCount( 2, $action_args ); - $this->assertSame( $action_args[0], $output ); + $this->assertSame( 1, did_action( 'wp_final_template_output_buffer' ) ); + $this->assertIsArray( $action_args, 'Expected the wp_final_template_output_buffer callback to have executed.' ); + $this->assertCount( 2, $action_args, 'Expected two params to be passed to the wp_final_template_output_buffer action.' ); + $this->assertSame( $action_args[1], $html_filter_args[1], 'Expected the $output param to match between the wp_template_output_buffer_html filter and the wp_final_template_output_buffer action.' ); + $this->assertNotSame( $action_args[1], $action_args[0], 'Expected the $output and $filtered_output args to not match when applying the wp_final_template_output_buffer action.' ); + $this->assertSame( $action_args[0], $output, 'Expected the filtered output buffer passed as the first param to the wp_final_template_output_buffer action to match the output buffer.' ); $this->assertStringContainsString( '', $action_args[0] ); $this->assertStringContainsString( '', $action_args[0] ); $this->assertStringContainsString( 'Saludo', $action_args[0] ); From b755a242765db5524fc5204996f402be25c3c3d1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 1 Oct 2025 14:27:52 -0700 Subject: [PATCH 14/32] Fix alignment --- tests/phpunit/tests/template.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index c511d394cb188..a11e8fb1942b4 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -527,7 +527,7 @@ function () use ( &$action_args ): void { add_filter( 'wp_template_output_buffer_html', static function ( string $buffer ) use ( &$html_filter_args ): string { - $html_filter_args = func_get_args(); + $html_filter_args = func_get_args(); $p = WP_HTML_Processor::create_full_parser( $buffer ); while ( $p->next_tag() ) { From 2dabd76840da581cbac85ee800e30ce13fd5984d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 10 Oct 2025 10:34:10 -0700 Subject: [PATCH 15/32] Clarify that the wp_before_include_template action occurs immediately before the template include --- src/wp-includes/template-loader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/template-loader.php b/src/wp-includes/template-loader.php index 2b9b9c4f90985..ffd2567524622 100644 --- a/src/wp-includes/template-loader.php +++ b/src/wp-includes/template-loader.php @@ -114,7 +114,7 @@ $template = apply_filters( 'template_include', $template ); if ( $template ) { /** - * Fires before including the template. + * Fires immediately before including the template. * * @since 6.9.0 * From e65811c6d8adaea507098029008f39b86507ed74 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 10 Oct 2025 10:39:33 -0700 Subject: [PATCH 16/32] Remove redundant/unreliable HTML content type check for the presence of tag --- src/wp-includes/template.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 9c77d0c40d99b..6fe4e106b4d02 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -890,11 +890,6 @@ function wp_finalize_template_output_buffer( string $output, int $phase ): strin } } - // If the content type is HTML, require that there be at least one tag. - if ( $is_html_content_type ) { - $is_html_content_type = ( new WP_HTML_Tag_Processor( $output ) )->next_tag(); - } - $filtered_output = $output; if ( $is_html_content_type ) { /** From 69df1d0f386756fc413072c7c72239cb7cade71a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 10 Oct 2025 13:39:26 -0700 Subject: [PATCH 17/32] Eliminate non-HTML output buffering and remove action --- src/wp-includes/template.php | 43 ++------ tests/phpunit/tests/template.php | 171 ++++++------------------------- 2 files changed, 43 insertions(+), 171 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 6fe4e106b4d02..90cb35198c33a 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -890,49 +890,24 @@ function wp_finalize_template_output_buffer( string $output, int $phase ): strin } } - $filtered_output = $output; - if ( $is_html_content_type ) { - /** - * Filters the HTML template output buffer prior to sending to the client. - * - * This filter only applies to HTML output, so it is safe for processing with the HTML API. - * Notice: It is highly discouraged to use regular expressions to do any kind of replacement on the output. - * - * @since 6.9.0 - * - * @param string $filtered_output Filtered output buffer. - * @param string $output Original output buffer. - * @return string Filtered output buffer HTML. - */ - $filtered_output = (string) apply_filters( 'wp_template_output_buffer_html', $filtered_output, $output ); + // If the content type is not HTML, short-circuit since it is not relevant optimization. + if ( ! $is_html_content_type ) { + return $output; } - /** - * Filters the template output buffer prior to sending to the client. - * - * This filter applies to HTML and non-HTML template output alike. For example, a "template" could return JSON. Use - * the {@see 'wp_template_output_buffer_html'} filter specifically for filtering HTML output, which is the normal - * case. Notice: It is highly discouraged to use regular expressions to do any kind of replacement on the output. - * - * @since 6.9.0 - * - * @param string $filtered_output Filtered output buffer. - * @param string $output Original output buffer. - * @return string Filtered output buffer. - */ - $filtered_output = (string) apply_filters( 'wp_template_output_buffer', $filtered_output, $output ); + $filtered_output = $output; /** - * Fires after the output buffer has been filtered prior to sending to the client. + * Filters the HTML template output buffer prior to sending to the client. * - * This is useful for caching plugins to capture the page output for storage. + * This filter only applies to HTML output, so it is safe for processing with the HTML API. + * Notice: It is highly discouraged to use regular expressions to do any kind of replacement on the output. * * @since 6.9.0 * * @param string $filtered_output Filtered output buffer. * @param string $output Original output buffer. + * @return string Filtered output buffer HTML. */ - do_action( 'wp_final_template_output_buffer', $filtered_output, $output ); - - return $filtered_output; + return (string) apply_filters( 'wp_template_output_buffer_html', $filtered_output, $output ); } diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index a11e8fb1942b4..fbf3bf83fe953 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -513,21 +513,11 @@ public function test_wp_start_template_output_buffer_for_html(): void { // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); - $action_args = null; - add_action( - 'wp_final_template_output_buffer', - function () use ( &$action_args ): void { - $action_args = func_get_args(); - }, - 10, - PHP_INT_MAX - ); - - $html_filter_args = null; + $filter_args = null; add_filter( 'wp_template_output_buffer_html', - static function ( string $buffer ) use ( &$html_filter_args ): string { - $html_filter_args = func_get_args(); + static function ( string $buffer ) use ( &$filter_args ): string { + $filter_args = func_get_args(); $p = WP_HTML_Processor::create_full_parser( $buffer ); while ( $p->next_tag() ) { @@ -552,21 +542,6 @@ static function ( string $buffer ) use ( &$html_filter_args ): string { PHP_INT_MAX ); - $filter_args = null; - add_filter( - 'wp_template_output_buffer', - function ( string $buffer ) use ( &$filter_args ): string { - $filter_args = func_get_args(); - $this->assertStringNotContainsString( 'Hello', $buffer ); - if ( str_starts_with( ltrim( $buffer ), '' ) ) { - $buffer .= "\n\n"; - } - return $buffer; - }, - 10, - PHP_INT_MAX - ); - $initial_ob_level = ob_get_level(); $this->assertTrue( wp_start_template_output_buffer() ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); @@ -597,40 +572,27 @@ function ( string $buffer ) use ( &$filter_args ): string { ob_end_flush(); // End the buffer started by wp_start_template_output_buffer(). $this->assertSame( $initial_ob_level, ob_get_level() ); - $this->assertIsArray( $html_filter_args, 'Expected the wp_template_output_buffer_html filter to have applied.' ); - $this->assertCount( 2, $html_filter_args, 'Expected two args to be supplied to the wp_template_output_buffer_html filter.' ); - $this->assertIsString( $html_filter_args[0], 'Expected the $filtered_output param to the wp_template_output_buffer_html filter to be a string.' ); - $this->assertIsString( $html_filter_args[1], 'Expected the $output param to the wp_template_output_buffer_html filter to be a string.' ); - $this->assertSame( $html_filter_args[1], $html_filter_args[0], 'Expected the initial $filtered_output to match $output in the wp_template_output_buffer_html filter.' ); - - $this->assertIsArray( $filter_args, 'Expected the wp_template_output_buffer filter to have applied.' ); - $this->assertCount( 2, $filter_args, 'Expected two args to be supplied to the wp_template_output_buffer filter.' ); - $this->assertIsString( $filter_args[0], 'Expected the $filtered_output param to the wp_template_output_buffer filter to be a string.' ); - $this->assertIsString( $filter_args[1], 'Expected the $output param to the wp_template_output_buffer filter to be a string.' ); - $this->assertNotSame( $filter_args[1], $filter_args[0], 'Expected the $output and $filtered_output args to not match when applying the wp_template_output_buffer filter since the first latter was modified by a wp_template_output_buffer_html filter.' ); - $this->assertSame( $filter_args[1], $html_filter_args[1], 'Expected the $output param to match between the wp_template_output_buffer and wp_template_output_buffer_html filters.' ); - - $output = ob_get_clean(); // Obtain the output via the wrapper output buffer. - $this->assertIsString( $output ); - $this->assertSame( 1, did_action( 'wp_final_template_output_buffer' ) ); - $this->assertIsArray( $action_args, 'Expected the wp_final_template_output_buffer callback to have executed.' ); - $this->assertCount( 2, $action_args, 'Expected two params to be passed to the wp_final_template_output_buffer action.' ); - $this->assertSame( $action_args[1], $html_filter_args[1], 'Expected the $output param to match between the wp_template_output_buffer_html filter and the wp_final_template_output_buffer action.' ); - $this->assertNotSame( $action_args[1], $action_args[0], 'Expected the $output and $filtered_output args to not match when applying the wp_final_template_output_buffer action.' ); - $this->assertSame( $action_args[0], $output, 'Expected the filtered output buffer passed as the first param to the wp_final_template_output_buffer action to match the output buffer.' ); - $this->assertStringContainsString( '', $action_args[0] ); - $this->assertStringContainsString( '', $action_args[0] ); - $this->assertStringContainsString( 'Saludo', $action_args[0] ); - $this->assertStringContainsString( '

¡Hola, mundo!

', $action_args[0] ); - $this->assertStringContainsString( '', $action_args[0] ); - $this->assertStringContainsString( '', $action_args[0] ); - $this->assertIsString( $action_args[1] ); - $this->assertStringContainsString( '', $action_args[1] ); - $this->assertStringContainsString( '', $action_args[1] ); - $this->assertStringContainsString( 'Greeting', $action_args[1] ); - $this->assertStringContainsString( '

Hello World!

', $action_args[1] ); - $this->assertStringContainsString( '', $action_args[1] ); - $this->assertStringNotContainsString( '', $action_args[1] ); + $this->assertIsArray( $filter_args, 'Expected the wp_template_output_buffer_html filter to have applied.' ); + $this->assertCount( 2, $filter_args, 'Expected two args to be supplied to the wp_template_output_buffer_html filter.' ); + $this->assertIsString( $filter_args[0], 'Expected the $filtered_output param to the wp_template_output_buffer_html filter to be a string.' ); + $this->assertIsString( $filter_args[1], 'Expected the $output param to the wp_template_output_buffer_html filter to be a string.' ); + $this->assertSame( $filter_args[1], $filter_args[0], 'Expected the initial $filtered_output to match $output in the wp_template_output_buffer_html filter.' ); + $original_output = $filter_args[0]; + $this->assertStringContainsString( '', $original_output ); + $this->assertStringContainsString( '', $original_output ); + $this->assertStringContainsString( 'Greeting', $original_output ); + $this->assertStringContainsString( '

Hello World!

', $original_output ); + $this->assertStringContainsString( '', $original_output ); + + $processed_output = ob_get_clean(); // Obtain the output via the wrapper output buffer. + $this->assertIsString( $processed_output ); + $this->assertNotEquals( $original_output, $processed_output ); + + $this->assertStringContainsString( '', $processed_output ); + $this->assertStringContainsString( '', $processed_output ); + $this->assertStringContainsString( 'Saludo', $processed_output ); + $this->assertStringContainsString( '

¡Hola, mundo!

', $processed_output ); + $this->assertStringContainsString( '', $processed_output ); } /** @@ -644,11 +606,11 @@ public function test_wp_start_template_output_buffer_ended_cleaned(): void { // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); - $called_html_filter = false; + $applied_filter = false; add_filter( 'wp_template_output_buffer_html', - static function ( string $buffer ) use ( &$called_html_filter ): string { - $called_html_filter = true; + static function ( string $buffer ) use ( &$applied_filter ): string { + $applied_filter = true; $p = WP_HTML_Processor::create_full_parser( $buffer ); if ( $p->next_tag( array( 'tag_name' => 'TITLE' ) ) ) { @@ -658,17 +620,6 @@ static function ( string $buffer ) use ( &$called_html_filter ): string { } ); - $called_filter = false; - add_filter( - 'wp_template_output_buffer', - function ( string $buffer ) use ( &$called_filter ): string { - $called_filter = true; - - $buffer .= "\n\n"; - return $buffer; - } - ); - $initial_ob_level = ob_get_level(); $this->assertTrue( wp_start_template_output_buffer() ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); @@ -696,8 +647,7 @@ function ( string $buffer ) use ( &$called_filter ): string { $this->assertSame( $initial_ob_level, ob_get_level() ); - $this->assertFalse( $called_html_filter ); - $this->assertFalse( $called_filter ); + $this->assertFalse( $applied_filter ); $this->assertSame( 0, did_action( 'wp_final_template_output_buffer' ) ); // Obtain the output via the wrapper output buffer. @@ -705,7 +655,6 @@ function ( string $buffer ) use ( &$called_filter ): string { $this->assertIsString( $output ); $this->assertStringNotContainsString( 'Unprocessed', $output ); $this->assertStringNotContainsString( 'Processed', $output ); - $this->assertStringNotContainsString( '', $output ); $this->assertStringContainsString( 'Output Buffer Not Processed', $output ); } @@ -734,17 +683,6 @@ static function ( string $buffer ) use ( &$called_html_filter ): string { } ); - $called_filter = false; - add_filter( - 'wp_template_output_buffer', - function ( string $buffer ) use ( &$called_filter ): string { - $called_filter = true; - - $buffer .= "\n\n"; - return $buffer; - } - ); - $initial_ob_level = ob_get_level(); $this->assertTrue( wp_start_template_output_buffer() ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); @@ -775,15 +713,12 @@ function ( string $buffer ) use ( &$called_filter ): string { $this->assertSame( $initial_ob_level, ob_get_level() ); $this->assertTrue( $called_html_filter ); - $this->assertTrue( $called_filter ); - $this->assertSame( 1, did_action( 'wp_final_template_output_buffer' ) ); // Obtain the output via the wrapper output buffer. $output = ob_get_clean(); $this->assertIsString( $output ); $this->assertStringNotContainsString( 'Unprocessed', $output ); $this->assertStringContainsString( 'Processed', $output ); - $this->assertStringContainsString( '', $output ); $this->assertStringContainsString( '

Template Replaced

', $output ); } @@ -798,43 +733,15 @@ public function test_wp_start_template_output_buffer_for_json(): void { // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); - $action_args = null; - add_action( - 'wp_final_template_output_buffer', - function () use ( &$action_args ): void { - $action_args = func_get_args(); - }, - 10, - PHP_INT_MAX - ); - - $called_html_filter = false; - add_filter( - 'wp_template_output_buffer_html', - static function ( string $buffer ) use ( &$called_html_filter ): string { - $called_html_filter = true; - return $buffer; - } - ); - - $called_filter = false; - add_filter( - 'wp_template_output_buffer', - function ( string $buffer ) use ( &$called_filter ): string { - $called_filter = true; - - $data = json_decode( $buffer, true ); - $data['data']['message'] = '¡Hola, mundo!'; - return json_encode( $data ); - } - ); + $mock_filter_callback = new MockAction(); + add_filter( 'wp_template_output_buffer_html', array( $mock_filter_callback, 'filter' ) ); $initial_ob_level = ob_get_level(); $this->assertTrue( wp_start_template_output_buffer() ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); ini_set( 'default_mimetype', 'application/json' ); // Since sending a header won't work. - echo wp_json_encode( + $json = wp_json_encode( array( 'success' => true, 'data' => array( @@ -843,6 +750,7 @@ function ( string $buffer ) use ( &$called_filter ): string { ), ) ); + echo $json; $ob_status = ob_get_status(); $this->assertSame( 'wp_finalize_template_output_buffer', $ob_status['name'], 'Expected name to be WP function.' ); @@ -852,23 +760,12 @@ function ( string $buffer ) use ( &$called_filter ): string { ob_end_flush(); // End the buffer started by wp_start_template_output_buffer(). $this->assertSame( $initial_ob_level, ob_get_level() ); - $this->assertFalse( $called_html_filter ); - $this->assertTrue( $called_filter ); - $this->assertSame( 1, did_action( 'wp_final_template_output_buffer' ) ); + $this->assertSame( 0, $mock_filter_callback->get_call_count() ); // Obtain the output via the wrapper output buffer. $output = ob_get_clean(); $this->assertIsString( $output ); - $this->assertIsArray( $action_args ); - $this->assertCount( 2, $action_args ); - $this->assertSame( $action_args[0], $output ); - $this->assertIsString( $action_args[1] ); - - $filtered_data = json_decode( $action_args[0], true ); - $original_data = json_decode( $action_args[1], true ); - - $this->assertSame( 'Hello, world!', $original_data['data']['message'] ); - $this->assertSame( '¡Hola, mundo!', $filtered_data['data']['message'] ); + $this->assertSame( $json, $output ); } public function assertTemplateHierarchy( $url, array $expected, $message = '' ) { From 853082e152a2425581ee52c6acd4cc5a45e8d4ff Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 10 Oct 2025 13:52:31 -0700 Subject: [PATCH 18/32] Reframe output buffer as being for optimization only Co-authored-by: Dennis Snell --- src/wp-includes/default-filters.php | 2 +- src/wp-includes/template.php | 30 ++++++------ tests/phpunit/tests/template.php | 72 ++++++++++++++--------------- 3 files changed, 54 insertions(+), 50 deletions(-) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 733783313481c..84c42ec454faf 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -422,7 +422,7 @@ add_action( 'do_all_pings', 'generic_ping', 10, 0 ); add_action( 'do_robots', 'do_robots' ); add_action( 'do_favicon', 'do_favicon' ); -add_action( 'wp_before_include_template', 'wp_start_template_output_buffer', 10, 1 ); +add_action( 'wp_before_include_template', 'wp_start_template_optimization_output_buffer', 10, 1 ); add_action( 'set_comment_cookies', 'wp_set_comment_cookies', 10, 3 ); add_action( 'sanitize_comment_cookies', 'sanitize_comment_cookies' ); add_action( 'init', 'smilies_init', 5 ); diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 90cb35198c33a..2f67f91a8a5de 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -825,7 +825,7 @@ function load_template( $_template_file, $load_once = true, $args = array() ) { } /** - * Starts the template output buffer. + * Starts the template optimization output buffer. * * This function is called immediately before the template is included. * @@ -833,9 +833,9 @@ function load_template( $_template_file, $load_once = true, $args = array() ) { * * @return bool Whether the output buffer successfully started. */ -function wp_start_template_output_buffer(): bool { +function wp_start_template_optimization_output_buffer(): bool { return ob_start( - 'wp_finalize_template_output_buffer', + 'wp_finalize_template_optimization_output_buffer', 0, // Unlimited buffer size so that entire output is passed to the filter. /* * Instead of the default PHP_OUTPUT_HANDLER_STDFLAGS (cleanable, flushable, and removable) being used for flags, @@ -857,17 +857,17 @@ function wp_start_template_output_buffer(): bool { } /** - * Finalizes the output buffer for the included template. + * Finalizes the template optimization output buffer. * * @since 6.9.0 * - * @see wp_start_template_output_buffer() + * @see wp_start_template_optimization_output_buffer() * * @param string $output Output buffer. * @param int $phase Phase. * @return string Finalized output buffer. */ -function wp_finalize_template_output_buffer( string $output, int $phase ): string { +function wp_finalize_template_optimization_output_buffer( string $output, int $phase ): string { // When the output is being cleaned (e.g. pending template is replaced with error page), do not send it through the filter. if ( ( $phase & PHP_OUTPUT_HANDLER_CLEAN ) !== 0 ) { return $output; @@ -898,16 +898,20 @@ function wp_finalize_template_output_buffer( string $output, int $phase ): strin $filtered_output = $output; /** - * Filters the HTML template output buffer prior to sending to the client. + * Filters the template optimization output buffer prior to sending to the client. * - * This filter only applies to HTML output, so it is safe for processing with the HTML API. - * Notice: It is highly discouraged to use regular expressions to do any kind of replacement on the output. + * This filter only applies the HTML output of an included template. This filter is a progressive enhancement + * intended for optimizing markup to improve frontend page load performance. Sites must not depend on this filter + * applying they may opt to stream the responses instead. Callbacks for this filter are highly discouraged from + * using regular expressions to do any kind of replacement on the output. Use the HTML API (either + * `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use PHP's DOM API, especially as of PHP 8.4 which fully + * supports HTML5. * * @since 6.9.0 * - * @param string $filtered_output Filtered output buffer. - * @param string $output Original output buffer. - * @return string Filtered output buffer HTML. + * @param string $filtered_output HTML template optimization output buffer. + * @param string $output Original HTML template output buffer. + * @return string HTML template optimization output buffer. */ - return (string) apply_filters( 'wp_template_output_buffer_html', $filtered_output, $output ); + return (string) apply_filters( 'wp_template_optimization_output_buffer', $filtered_output, $output ); } diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index fbf3bf83fe953..8706c15e8dd24 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -502,20 +502,20 @@ public function test_locate_template_uses_current_theme() { } /** - * Tests that wp_start_template_output_buffer() starts the expected output buffer and that the expected hooks fire for + * Tests that wp_start_template_optimization_output_buffer() starts the expected output buffer and that the expected hooks fire for * an HTML document and that the response is not incrementally flushable. * * @ticket 43258 - * @covers ::wp_start_template_output_buffer - * @covers ::wp_finalize_template_output_buffers + * @covers ::wp_start_template_optimization_output_buffer + * @covers ::wp_finalize_template_optimization_output_buffer */ - public function test_wp_start_template_output_buffer_for_html(): void { + public function test_wp_start_template_optimization_output_buffer_for_html(): void { // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); $filter_args = null; add_filter( - 'wp_template_output_buffer_html', + 'wp_template_optimization_output_buffer', static function ( string $buffer ) use ( &$filter_args ): string { $filter_args = func_get_args(); @@ -543,7 +543,7 @@ static function ( string $buffer ) use ( &$filter_args ): string { ); $initial_ob_level = ob_get_level(); - $this->assertTrue( wp_start_template_output_buffer() ); + $this->assertTrue( wp_start_template_optimization_output_buffer() ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); ?> @@ -565,18 +565,18 @@ static function ( string $buffer ) use ( &$filter_args ): string { assertSame( 'wp_finalize_template_output_buffer', $ob_status['name'], 'Expected name to be WP function.' ); + $this->assertSame( 'wp_finalize_template_optimization_output_buffer', $ob_status['name'], 'Expected name to be WP function.' ); $this->assertSame( 1, $ob_status['type'], 'Expected type to be user supplied handler.' ); $this->assertSame( 0, $ob_status['chunk_size'], 'Expected unlimited chunk size.' ); - ob_end_flush(); // End the buffer started by wp_start_template_output_buffer(). + ob_end_flush(); // End the buffer started by wp_start_template_optimization_output_buffer(). $this->assertSame( $initial_ob_level, ob_get_level() ); - $this->assertIsArray( $filter_args, 'Expected the wp_template_output_buffer_html filter to have applied.' ); - $this->assertCount( 2, $filter_args, 'Expected two args to be supplied to the wp_template_output_buffer_html filter.' ); - $this->assertIsString( $filter_args[0], 'Expected the $filtered_output param to the wp_template_output_buffer_html filter to be a string.' ); - $this->assertIsString( $filter_args[1], 'Expected the $output param to the wp_template_output_buffer_html filter to be a string.' ); - $this->assertSame( $filter_args[1], $filter_args[0], 'Expected the initial $filtered_output to match $output in the wp_template_output_buffer_html filter.' ); + $this->assertIsArray( $filter_args, 'Expected the wp_template_optimization_output_buffer filter to have applied.' ); + $this->assertCount( 2, $filter_args, 'Expected two args to be supplied to the wp_template_optimization_output_buffer filter.' ); + $this->assertIsString( $filter_args[0], 'Expected the $filtered_output param to the wp_template_optimization_output_buffer filter to be a string.' ); + $this->assertIsString( $filter_args[1], 'Expected the $output param to the wp_template_optimization_output_buffer filter to be a string.' ); + $this->assertSame( $filter_args[1], $filter_args[0], 'Expected the initial $filtered_output to match $output in the wp_template_optimization_output_buffer filter.' ); $original_output = $filter_args[0]; $this->assertStringContainsString( '', $original_output ); $this->assertStringContainsString( '', $original_output ); @@ -596,19 +596,19 @@ static function ( string $buffer ) use ( &$filter_args ): string { } /** - * Tests that wp_start_template_output_buffer() starts the expected output buffer but ending with cleaning prevents any processing. + * Tests that wp_start_template_optimization_output_buffer() starts the expected output buffer but ending with cleaning prevents any processing. * * @ticket 43258 - * @covers ::wp_start_template_output_buffer - * @covers ::wp_finalize_template_output_buffer + * @covers ::wp_start_template_optimization_output_buffer + * @covers ::wp_finalize_template_optimization_output_buffer */ - public function test_wp_start_template_output_buffer_ended_cleaned(): void { + public function test_wp_start_template_optimization_output_buffer_ended_cleaned(): void { // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); $applied_filter = false; add_filter( - 'wp_template_output_buffer_html', + 'wp_template_optimization_output_buffer', static function ( string $buffer ) use ( &$applied_filter ): string { $applied_filter = true; @@ -621,7 +621,7 @@ static function ( string $buffer ) use ( &$applied_filter ): string { ); $initial_ob_level = ob_get_level(); - $this->assertTrue( wp_start_template_output_buffer() ); + $this->assertTrue( wp_start_template_optimization_output_buffer() ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); ?> @@ -633,7 +633,7 @@ static function ( string $buffer ) use ( &$applied_filter ): string {

Hello World!

- + @@ -659,19 +659,19 @@ static function ( string $buffer ) use ( &$applied_filter ): string { } /** - * Tests that wp_start_template_output_buffer() starts the expected output buffer and cleaning allows the template to be replaced. + * Tests that wp_start_template_optimization_output_buffer() starts the expected output buffer and cleaning allows the template to be replaced. * * @ticket 43258 - * @covers ::wp_start_template_output_buffer - * @covers ::wp_finalize_template_output_buffer + * @covers ::wp_start_template_optimization_output_buffer + * @covers ::wp_finalize_template_optimization_output_buffer */ - public function test_wp_start_template_output_buffer_cleaned_and_replaced(): void { + public function test_wp_start_template_optimization_output_buffer_cleaned_and_replaced(): void { // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); $called_html_filter = false; add_filter( - 'wp_template_output_buffer_html', + 'wp_template_optimization_output_buffer', static function ( string $buffer ) use ( &$called_html_filter ): string { $called_html_filter = true; @@ -684,7 +684,7 @@ static function ( string $buffer ) use ( &$called_html_filter ): string { ); $initial_ob_level = ob_get_level(); - $this->assertTrue( wp_start_template_output_buffer() ); + $this->assertTrue( wp_start_template_optimization_output_buffer() ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); ?> @@ -696,7 +696,7 @@ static function ( string $buffer ) use ( &$called_html_filter ): string {

Hello World!

- + @@ -709,7 +709,7 @@ static function ( string $buffer ) use ( &$called_html_filter ): string { assertSame( $initial_ob_level, ob_get_level() ); $this->assertTrue( $called_html_filter ); @@ -723,21 +723,21 @@ static function ( string $buffer ) use ( &$called_html_filter ): string { } /** - * Tests that wp_start_template_output_buffer() starts the expected output buffer and that the expected hooks fire for JSON response. + * Tests that wp_start_template_optimization_output_buffer() starts the expected output buffer and that the expected hooks fire for JSON response. * * @ticket 43258 - * @covers ::wp_start_template_output_buffer - * @covers ::wp_finalize_template_output_buffer + * @covers ::wp_start_template_optimization_output_buffer + * @covers ::wp_finalize_template_optimization_output_buffer */ - public function test_wp_start_template_output_buffer_for_json(): void { + public function test_wp_start_template_optimization_output_buffer_for_json(): void { // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); $mock_filter_callback = new MockAction(); - add_filter( 'wp_template_output_buffer_html', array( $mock_filter_callback, 'filter' ) ); + add_filter( 'wp_template_optimization_output_buffer', array( $mock_filter_callback, 'filter' ) ); $initial_ob_level = ob_get_level(); - $this->assertTrue( wp_start_template_output_buffer() ); + $this->assertTrue( wp_start_template_optimization_output_buffer() ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); ini_set( 'default_mimetype', 'application/json' ); // Since sending a header won't work. @@ -753,11 +753,11 @@ public function test_wp_start_template_output_buffer_for_json(): void { echo $json; $ob_status = ob_get_status(); - $this->assertSame( 'wp_finalize_template_output_buffer', $ob_status['name'], 'Expected name to be WP function.' ); + $this->assertSame( 'wp_finalize_template_optimization_output_buffer', $ob_status['name'], 'Expected name to be WP function.' ); $this->assertSame( 1, $ob_status['type'], 'Expected type to be user supplied handler.' ); $this->assertSame( 0, $ob_status['chunk_size'], 'Expected unlimited chunk size.' ); - ob_end_flush(); // End the buffer started by wp_start_template_output_buffer(). + ob_end_flush(); // End the buffer started by wp_start_template_optimization_output_buffer(). $this->assertSame( $initial_ob_level, ob_get_level() ); $this->assertSame( 0, $mock_filter_callback->get_call_count() ); From 7f309f5861d4d314189b125a7c58be78a3ac727c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 10 Oct 2025 14:10:41 -0700 Subject: [PATCH 19/32] Only start output buffer if filters are present by default --- src/wp-includes/template.php | 15 ++++++++++ tests/phpunit/tests/template.php | 49 +++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 2f67f91a8a5de..766d3ebec7ccc 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -834,6 +834,21 @@ function load_template( $_template_file, $load_once = true, $args = array() ) { * @return bool Whether the output buffer successfully started. */ function wp_start_template_optimization_output_buffer(): bool { + /** + * Filters whether the template will be output-buffered for optimization. + * + * By default, an output buffer is only started if a {@see 'wp_template_optimization_output_buffer'} filter has been + * added. For this default to apply, a filter must be added by the time the template is included at the + * {@see 'wp_before_include_template'} action. This allows template responses to be streamed as much as possible + * when no template optimizations are registered to apply. This filter allows a site to opt in to adding such + * template optimization filters during the rendering of the template. + * + * @since 6.9.0 + */ + if ( ! apply_filters( 'wp_template_output_buffered_for_optimization', has_filter( 'wp_template_optimization_output_buffer' ) ) ) { + return false; + } + return ob_start( 'wp_finalize_template_optimization_output_buffer', 0, // Unlimited buffer size so that entire output is passed to the filter. diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 8706c15e8dd24..082c7ec2975a1 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -501,6 +501,53 @@ public function test_locate_template_uses_current_theme() { $this->assertSame( $new_theme->get_stylesheet_directory() . '/index.php', locate_template( $template_names ), 'Incorrect index template found in theme after switch.' ); } + /** + * Tests that wp_start_template_optimization_output_buffer() does not start a buffer when no filters are present. + * + * @ticket 43258 + * @covers ::wp_start_template_optimization_output_buffer + */ + public function test_wp_start_template_optimization_output_buffer_without_filters_and_no_override(): void { + remove_all_filters( 'wp_template_optimization_output_buffer' ); + $level = ob_get_level(); + $this->assertFalse( wp_start_template_optimization_output_buffer() ); + $this->assertSame( $level, ob_get_level() ); + } + + /** + * Tests that wp_start_template_optimization_output_buffer() does start a buffer when no filters are present but there is an override. + * + * @ticket 43258 + * @covers ::wp_start_template_optimization_output_buffer + */ + public function test_wp_start_template_optimization_output_buffer_begins_without_filters_but_overridden(): void { + remove_all_filters( 'wp_template_optimization_output_buffer' ); + $level = ob_get_level(); + add_filter( 'wp_template_output_buffered_for_optimization', '__return_true' ); + $this->assertTrue( wp_start_template_optimization_output_buffer() ); + $this->assertSame( $level + 1, ob_get_level() ); + ob_end_clean(); + } + + /** + * Tests that wp_start_template_optimization_output_buffer() does not start a buffer even when there are filters present due to override. + * + * @ticket 43258 + * @covers ::wp_start_template_optimization_output_buffer + */ + public function test_wp_start_template_optimization_output_buffer_begins_with_filters_but_blocked(): void { + add_filter( + 'wp_template_optimization_output_buffer', + static function () { + return 'Hey!'; + } + ); + $level = ob_get_level(); + add_filter( 'wp_template_output_buffered_for_optimization', '__return_false' ); + $this->assertFalse( wp_start_template_optimization_output_buffer() ); + $this->assertSame( $level, ob_get_level() ); + } + /** * Tests that wp_start_template_optimization_output_buffer() starts the expected output buffer and that the expected hooks fire for * an HTML document and that the response is not incrementally flushable. @@ -723,7 +770,7 @@ static function ( string $buffer ) use ( &$called_html_filter ): string { } /** - * Tests that wp_start_template_optimization_output_buffer() starts the expected output buffer and that the expected hooks fire for JSON response. + * Tests that wp_start_template_optimization_output_buffer() starts the expected output buffer and that the output buffer is not processed. * * @ticket 43258 * @covers ::wp_start_template_optimization_output_buffer From 6ee2a45da894892d787e1ba6751d9e5f7c3eaa22 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 11 Oct 2025 11:24:20 -0700 Subject: [PATCH 20/32] Account for only the first listed content-type header being sent Co-authored-by: Dennis Snell --- src/wp-includes/template.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 766d3ebec7ccc..debcfece587ca 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -889,21 +889,22 @@ function wp_finalize_template_optimization_output_buffer( string $output, int $p } // Detect if the response is an HTML content type. - $is_html_content_type = false; - $headers_list = array_merge( - array( 'Content-Type: ' . ini_get( 'default_mimetype' ) ), - headers_list() - ); - foreach ( $headers_list as $header ) { + $is_html_content_type = null; + $html_content_types = array( 'text/html', 'application/xhtml+xml' ); + foreach ( headers_list() as $header ) { $header_parts = preg_split( '/\s*[:;]\s*/', strtolower( $header ) ); if ( is_array( $header_parts ) && count( $header_parts ) >= 2 && 'content-type' === $header_parts[0] ) { - $is_html_content_type = in_array( $header_parts[1], array( 'text/html', 'application/xhtml+xml' ), true ); + $is_html_content_type = in_array( $header_parts[1], $html_content_types, true ); + break; // PHP only sends the first Content-Type header in the list. } } + if ( null === $is_html_content_type ) { + $is_html_content_type = in_array( ini_get( 'default_mimetype' ), $html_content_types, true ); + } // If the content type is not HTML, short-circuit since it is not relevant optimization. if ( ! $is_html_content_type ) { From 1b4a2c421008872fc0301404a1a9607f48c5dd95 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 11 Oct 2025 11:25:35 -0700 Subject: [PATCH 21/32] Add missing filter param to phpdoc --- src/wp-includes/template.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index debcfece587ca..35a171e2a8b06 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -844,6 +844,8 @@ function wp_start_template_optimization_output_buffer(): bool { * template optimization filters during the rendering of the template. * * @since 6.9.0 + * + * @param bool $use_output_buffer Whether an output buffer is started. */ if ( ! apply_filters( 'wp_template_output_buffered_for_optimization', has_filter( 'wp_template_optimization_output_buffer' ) ) ) { return false; From 7f43ee54fabf9776025c8c71f52eb87f9f51ab93 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 11 Oct 2025 11:30:54 -0700 Subject: [PATCH 22/32] Fix typo Co-authored-by: Dennis Snell --- src/wp-includes/template.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 35a171e2a8b06..877bad9e991b0 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -920,8 +920,8 @@ function wp_finalize_template_optimization_output_buffer( string $output, int $p * * This filter only applies the HTML output of an included template. This filter is a progressive enhancement * intended for optimizing markup to improve frontend page load performance. Sites must not depend on this filter - * applying they may opt to stream the responses instead. Callbacks for this filter are highly discouraged from - * using regular expressions to do any kind of replacement on the output. Use the HTML API (either + * applying since they may opt to stream the responses instead. Callbacks for this filter are highly discouraged + * from using regular expressions to do any kind of replacement on the output. Use the HTML API (either * `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use PHP's DOM API, especially as of PHP 8.4 which fully * supports HTML5. * From b242dc030e5f3ca73ee258e62b9ba330a8528df5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 11 Oct 2025 11:32:12 -0700 Subject: [PATCH 23/32] Be explicit about the DOM API to use Co-authored-by: Dennis Snell --- src/wp-includes/template.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 877bad9e991b0..ec8b7abe0db57 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -922,7 +922,7 @@ function wp_finalize_template_optimization_output_buffer( string $output, int $p * intended for optimizing markup to improve frontend page load performance. Sites must not depend on this filter * applying since they may opt to stream the responses instead. Callbacks for this filter are highly discouraged * from using regular expressions to do any kind of replacement on the output. Use the HTML API (either - * `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use PHP's DOM API, especially as of PHP 8.4 which fully + * `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use {@see DOM\HtmlDocument} as of PHP 8.4 which fully * supports HTML5. * * @since 6.9.0 From ad4c8d4b56e507e29f4bd9a19a51c619d3352c0d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 11 Oct 2025 11:41:08 -0700 Subject: [PATCH 24/32] Clarify purpose of filter and why chunking is disabled Co-authored-by: Dennis Snell --- src/wp-includes/template.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index ec8b7abe0db57..c9c8ad99bca61 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -855,10 +855,13 @@ function wp_start_template_optimization_output_buffer(): bool { 'wp_finalize_template_optimization_output_buffer', 0, // Unlimited buffer size so that entire output is passed to the filter. /* - * Instead of the default PHP_OUTPUT_HANDLER_STDFLAGS (cleanable, flushable, and removable) being used for flags, - * the PHP_OUTPUT_HANDLER_FLUSHABLE flag must be omitted. If the buffer were flushable, then each time that - * ob_flush() is called, it would send a fragment of the output into the output buffer callback. When buffering the - * entire response as an HTML document, this would result in broken HTML processing. + * Instead of the default PHP_OUTPUT_HANDLER_STDFLAGS (cleanable, flushable, and removable) being used for + * flags, the PHP_OUTPUT_HANDLER_FLUSHABLE flag must be omitted. If the buffer were flushable, then each time + * that ob_flush() is called, a fragment of the output would be sent into the output buffer callback. This + * output buffer is intended to capture the entire response for processing, as indicated by the chunk size of 0. + * So the buffer does not allow flushing to ensure the entire buffer can be processed, such as for optimizing an + * entire HTML document, where markup in the HEAD may need to be adjusted based on markup that appears late in + * the BODY. * * If this ends up being problematic, then PHP_OUTPUT_HANDLER_FLUSHABLE could be added to the $flags and the * output buffer callback could check if the phase is PHP_OUTPUT_HANDLER_FLUSH and abort any subsequent From 80d24ae2826ac9df7847a21827805027e8f143b6 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 11 Oct 2025 11:46:56 -0700 Subject: [PATCH 25/32] Refer to enhancement rather than optimization Co-authored-by: Dennis Snell --- src/wp-includes/default-filters.php | 2 +- src/wp-includes/template.php | 42 +++++------ tests/phpunit/tests/template.php | 106 ++++++++++++++-------------- 3 files changed, 75 insertions(+), 75 deletions(-) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 41b8329296526..1a765158a63f2 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -422,7 +422,7 @@ add_action( 'do_all_pings', 'generic_ping', 10, 0 ); add_action( 'do_robots', 'do_robots' ); add_action( 'do_favicon', 'do_favicon' ); -add_action( 'wp_before_include_template', 'wp_start_template_optimization_output_buffer', 10, 1 ); +add_action( 'wp_before_include_template', 'wp_start_template_enhancement_output_buffer', 10, 1 ); add_action( 'set_comment_cookies', 'wp_set_comment_cookies', 10, 3 ); add_action( 'sanitize_comment_cookies', 'sanitize_comment_cookies' ); add_action( 'init', 'smilies_init', 5 ); diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index c9c8ad99bca61..78b03b4678a3e 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -825,7 +825,7 @@ function load_template( $_template_file, $load_once = true, $args = array() ) { } /** - * Starts the template optimization output buffer. + * Starts the template enhancement output buffer. * * This function is called immediately before the template is included. * @@ -833,26 +833,26 @@ function load_template( $_template_file, $load_once = true, $args = array() ) { * * @return bool Whether the output buffer successfully started. */ -function wp_start_template_optimization_output_buffer(): bool { +function wp_start_template_enhancement_output_buffer(): bool { /** - * Filters whether the template will be output-buffered for optimization. + * Filters whether the template will be output-buffered for enhancement. * - * By default, an output buffer is only started if a {@see 'wp_template_optimization_output_buffer'} filter has been + * By default, an output buffer is only started if a {@see 'wp_template_enhancement_output_buffer'} filter has been * added. For this default to apply, a filter must be added by the time the template is included at the * {@see 'wp_before_include_template'} action. This allows template responses to be streamed as much as possible - * when no template optimizations are registered to apply. This filter allows a site to opt in to adding such - * template optimization filters during the rendering of the template. + * when no template enhancements are registered to apply. This filter allows a site to opt in to adding such + * template enhancement filters during the rendering of the template. * * @since 6.9.0 * * @param bool $use_output_buffer Whether an output buffer is started. */ - if ( ! apply_filters( 'wp_template_output_buffered_for_optimization', has_filter( 'wp_template_optimization_output_buffer' ) ) ) { + if ( ! apply_filters( 'wp_template_output_buffered_for_enhancement', has_filter( 'wp_template_enhancement_output_buffer' ) ) ) { return false; } return ob_start( - 'wp_finalize_template_optimization_output_buffer', + 'wp_finalize_template_enhancement_output_buffer', 0, // Unlimited buffer size so that entire output is passed to the filter. /* * Instead of the default PHP_OUTPUT_HANDLER_STDFLAGS (cleanable, flushable, and removable) being used for @@ -877,17 +877,17 @@ function wp_start_template_optimization_output_buffer(): bool { } /** - * Finalizes the template optimization output buffer. + * Finalizes the template enhancement output buffer. * * @since 6.9.0 * - * @see wp_start_template_optimization_output_buffer() + * @see wp_start_template_enhancement_output_buffer() * * @param string $output Output buffer. * @param int $phase Phase. * @return string Finalized output buffer. */ -function wp_finalize_template_optimization_output_buffer( string $output, int $phase ): string { +function wp_finalize_template_enhancement_output_buffer( string $output, int $phase ): string { // When the output is being cleaned (e.g. pending template is replaced with error page), do not send it through the filter. if ( ( $phase & PHP_OUTPUT_HANDLER_CLEAN ) !== 0 ) { return $output; @@ -911,7 +911,7 @@ function wp_finalize_template_optimization_output_buffer( string $output, int $p $is_html_content_type = in_array( ini_get( 'default_mimetype' ), $html_content_types, true ); } - // If the content type is not HTML, short-circuit since it is not relevant optimization. + // If the content type is not HTML, short-circuit since it is not relevant for enhancement. if ( ! $is_html_content_type ) { return $output; } @@ -919,20 +919,20 @@ function wp_finalize_template_optimization_output_buffer( string $output, int $p $filtered_output = $output; /** - * Filters the template optimization output buffer prior to sending to the client. + * Filters the template enhancement output buffer prior to sending to the client. * * This filter only applies the HTML output of an included template. This filter is a progressive enhancement - * intended for optimizing markup to improve frontend page load performance. Sites must not depend on this filter - * applying since they may opt to stream the responses instead. Callbacks for this filter are highly discouraged - * from using regular expressions to do any kind of replacement on the output. Use the HTML API (either - * `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use {@see DOM\HtmlDocument} as of PHP 8.4 which fully - * supports HTML5. + * intended for applications such as optimizing markup to improve frontend page load performance. Sites must not + * depend on this filter applying since they may opt to stream the responses instead. Callbacks for this filter are + * highly discouraged from using regular expressions to do any kind of replacement on the output. Use the HTML API + * (either `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use {@see DOM\HtmlDocument} as of PHP 8.4 which + * fully supports HTML5. * * @since 6.9.0 * - * @param string $filtered_output HTML template optimization output buffer. + * @param string $filtered_output HTML template enhancement output buffer. * @param string $output Original HTML template output buffer. - * @return string HTML template optimization output buffer. + * @return string HTML template enhancement output buffer. */ - return (string) apply_filters( 'wp_template_optimization_output_buffer', $filtered_output, $output ); + return (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output ); } diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 082c7ec2975a1..1656891e93505 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -502,67 +502,67 @@ public function test_locate_template_uses_current_theme() { } /** - * Tests that wp_start_template_optimization_output_buffer() does not start a buffer when no filters are present. + * Tests that wp_start_template_enhancement_output_buffer() does not start a buffer when no filters are present. * * @ticket 43258 - * @covers ::wp_start_template_optimization_output_buffer + * @covers ::wp_start_template_enhancement_output_buffer */ - public function test_wp_start_template_optimization_output_buffer_without_filters_and_no_override(): void { - remove_all_filters( 'wp_template_optimization_output_buffer' ); + public function test_wp_start_template_enhancement_output_buffer_without_filters_and_no_override(): void { + remove_all_filters( 'wp_template_enhancement_output_buffer' ); $level = ob_get_level(); - $this->assertFalse( wp_start_template_optimization_output_buffer() ); + $this->assertFalse( wp_start_template_enhancement_output_buffer() ); $this->assertSame( $level, ob_get_level() ); } /** - * Tests that wp_start_template_optimization_output_buffer() does start a buffer when no filters are present but there is an override. + * Tests that wp_start_template_enhancement_output_buffer() does start a buffer when no filters are present but there is an override. * * @ticket 43258 - * @covers ::wp_start_template_optimization_output_buffer + * @covers ::wp_start_template_enhancement_output_buffer */ - public function test_wp_start_template_optimization_output_buffer_begins_without_filters_but_overridden(): void { - remove_all_filters( 'wp_template_optimization_output_buffer' ); + public function test_wp_start_template_enhancement_output_buffer_begins_without_filters_but_overridden(): void { + remove_all_filters( 'wp_template_enhancement_output_buffer' ); $level = ob_get_level(); - add_filter( 'wp_template_output_buffered_for_optimization', '__return_true' ); - $this->assertTrue( wp_start_template_optimization_output_buffer() ); + add_filter( 'wp_template_output_buffered_for_enhancement', '__return_true' ); + $this->assertTrue( wp_start_template_enhancement_output_buffer() ); $this->assertSame( $level + 1, ob_get_level() ); ob_end_clean(); } /** - * Tests that wp_start_template_optimization_output_buffer() does not start a buffer even when there are filters present due to override. + * Tests that wp_start_template_enhancement_output_buffer() does not start a buffer even when there are filters present due to override. * * @ticket 43258 - * @covers ::wp_start_template_optimization_output_buffer + * @covers ::wp_start_template_enhancement_output_buffer */ - public function test_wp_start_template_optimization_output_buffer_begins_with_filters_but_blocked(): void { + public function test_wp_start_template_enhancement_output_buffer_begins_with_filters_but_blocked(): void { add_filter( - 'wp_template_optimization_output_buffer', + 'wp_template_enhancement_output_buffer', static function () { return 'Hey!'; } ); $level = ob_get_level(); - add_filter( 'wp_template_output_buffered_for_optimization', '__return_false' ); - $this->assertFalse( wp_start_template_optimization_output_buffer() ); + add_filter( 'wp_template_output_buffered_for_enhancement', '__return_false' ); + $this->assertFalse( wp_start_template_enhancement_output_buffer() ); $this->assertSame( $level, ob_get_level() ); } /** - * Tests that wp_start_template_optimization_output_buffer() starts the expected output buffer and that the expected hooks fire for + * Tests that wp_start_template_enhancement_output_buffer() starts the expected output buffer and that the expected hooks fire for * an HTML document and that the response is not incrementally flushable. * * @ticket 43258 - * @covers ::wp_start_template_optimization_output_buffer - * @covers ::wp_finalize_template_optimization_output_buffer + * @covers ::wp_start_template_enhancement_output_buffer + * @covers ::wp_finalize_template_enhancement_output_buffer */ - public function test_wp_start_template_optimization_output_buffer_for_html(): void { + public function test_wp_start_template_enhancement_output_buffer_for_html(): void { // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); $filter_args = null; add_filter( - 'wp_template_optimization_output_buffer', + 'wp_template_enhancement_output_buffer', static function ( string $buffer ) use ( &$filter_args ): string { $filter_args = func_get_args(); @@ -590,7 +590,7 @@ static function ( string $buffer ) use ( &$filter_args ): string { ); $initial_ob_level = ob_get_level(); - $this->assertTrue( wp_start_template_optimization_output_buffer() ); + $this->assertTrue( wp_start_template_enhancement_output_buffer() ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); ?> @@ -612,18 +612,18 @@ static function ( string $buffer ) use ( &$filter_args ): string { assertSame( 'wp_finalize_template_optimization_output_buffer', $ob_status['name'], 'Expected name to be WP function.' ); + $this->assertSame( 'wp_finalize_template_enhancement_output_buffer', $ob_status['name'], 'Expected name to be WP function.' ); $this->assertSame( 1, $ob_status['type'], 'Expected type to be user supplied handler.' ); $this->assertSame( 0, $ob_status['chunk_size'], 'Expected unlimited chunk size.' ); - ob_end_flush(); // End the buffer started by wp_start_template_optimization_output_buffer(). + ob_end_flush(); // End the buffer started by wp_start_template_enhancement_output_buffer(). $this->assertSame( $initial_ob_level, ob_get_level() ); - $this->assertIsArray( $filter_args, 'Expected the wp_template_optimization_output_buffer filter to have applied.' ); - $this->assertCount( 2, $filter_args, 'Expected two args to be supplied to the wp_template_optimization_output_buffer filter.' ); - $this->assertIsString( $filter_args[0], 'Expected the $filtered_output param to the wp_template_optimization_output_buffer filter to be a string.' ); - $this->assertIsString( $filter_args[1], 'Expected the $output param to the wp_template_optimization_output_buffer filter to be a string.' ); - $this->assertSame( $filter_args[1], $filter_args[0], 'Expected the initial $filtered_output to match $output in the wp_template_optimization_output_buffer filter.' ); + $this->assertIsArray( $filter_args, 'Expected the wp_template_enhancement_output_buffer filter to have applied.' ); + $this->assertCount( 2, $filter_args, 'Expected two args to be supplied to the wp_template_enhancement_output_buffer filter.' ); + $this->assertIsString( $filter_args[0], 'Expected the $filtered_output param to the wp_template_enhancement_output_buffer filter to be a string.' ); + $this->assertIsString( $filter_args[1], 'Expected the $output param to the wp_template_enhancement_output_buffer filter to be a string.' ); + $this->assertSame( $filter_args[1], $filter_args[0], 'Expected the initial $filtered_output to match $output in the wp_template_enhancement_output_buffer filter.' ); $original_output = $filter_args[0]; $this->assertStringContainsString( '', $original_output ); $this->assertStringContainsString( '', $original_output ); @@ -643,19 +643,19 @@ static function ( string $buffer ) use ( &$filter_args ): string { } /** - * Tests that wp_start_template_optimization_output_buffer() starts the expected output buffer but ending with cleaning prevents any processing. + * Tests that wp_start_template_enhancement_output_buffer() starts the expected output buffer but ending with cleaning prevents any processing. * * @ticket 43258 - * @covers ::wp_start_template_optimization_output_buffer - * @covers ::wp_finalize_template_optimization_output_buffer + * @covers ::wp_start_template_enhancement_output_buffer + * @covers ::wp_finalize_template_enhancement_output_buffer */ - public function test_wp_start_template_optimization_output_buffer_ended_cleaned(): void { + public function test_wp_start_template_enhancement_output_buffer_ended_cleaned(): void { // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); $applied_filter = false; add_filter( - 'wp_template_optimization_output_buffer', + 'wp_template_enhancement_output_buffer', static function ( string $buffer ) use ( &$applied_filter ): string { $applied_filter = true; @@ -668,7 +668,7 @@ static function ( string $buffer ) use ( &$applied_filter ): string { ); $initial_ob_level = ob_get_level(); - $this->assertTrue( wp_start_template_optimization_output_buffer() ); + $this->assertTrue( wp_start_template_enhancement_output_buffer() ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); ?> @@ -680,7 +680,7 @@ static function ( string $buffer ) use ( &$applied_filter ): string {

Hello World!

- + @@ -706,19 +706,19 @@ static function ( string $buffer ) use ( &$applied_filter ): string { } /** - * Tests that wp_start_template_optimization_output_buffer() starts the expected output buffer and cleaning allows the template to be replaced. + * Tests that wp_start_template_enhancement_output_buffer() starts the expected output buffer and cleaning allows the template to be replaced. * * @ticket 43258 - * @covers ::wp_start_template_optimization_output_buffer - * @covers ::wp_finalize_template_optimization_output_buffer + * @covers ::wp_start_template_enhancement_output_buffer + * @covers ::wp_finalize_template_enhancement_output_buffer */ - public function test_wp_start_template_optimization_output_buffer_cleaned_and_replaced(): void { + public function test_wp_start_template_enhancement_output_buffer_cleaned_and_replaced(): void { // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); $called_html_filter = false; add_filter( - 'wp_template_optimization_output_buffer', + 'wp_template_enhancement_output_buffer', static function ( string $buffer ) use ( &$called_html_filter ): string { $called_html_filter = true; @@ -731,7 +731,7 @@ static function ( string $buffer ) use ( &$called_html_filter ): string { ); $initial_ob_level = ob_get_level(); - $this->assertTrue( wp_start_template_optimization_output_buffer() ); + $this->assertTrue( wp_start_template_enhancement_output_buffer() ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); ?> @@ -743,7 +743,7 @@ static function ( string $buffer ) use ( &$called_html_filter ): string {

Hello World!

- + @@ -756,7 +756,7 @@ static function ( string $buffer ) use ( &$called_html_filter ): string { assertSame( $initial_ob_level, ob_get_level() ); $this->assertTrue( $called_html_filter ); @@ -770,21 +770,21 @@ static function ( string $buffer ) use ( &$called_html_filter ): string { } /** - * Tests that wp_start_template_optimization_output_buffer() starts the expected output buffer and that the output buffer is not processed. + * Tests that wp_start_template_enhancement_output_buffer() starts the expected output buffer and that the output buffer is not processed. * * @ticket 43258 - * @covers ::wp_start_template_optimization_output_buffer - * @covers ::wp_finalize_template_optimization_output_buffer + * @covers ::wp_start_template_enhancement_output_buffer + * @covers ::wp_finalize_template_enhancement_output_buffer */ - public function test_wp_start_template_optimization_output_buffer_for_json(): void { + public function test_wp_start_template_enhancement_output_buffer_for_json(): void { // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); $mock_filter_callback = new MockAction(); - add_filter( 'wp_template_optimization_output_buffer', array( $mock_filter_callback, 'filter' ) ); + add_filter( 'wp_template_enhancement_output_buffer', array( $mock_filter_callback, 'filter' ) ); $initial_ob_level = ob_get_level(); - $this->assertTrue( wp_start_template_optimization_output_buffer() ); + $this->assertTrue( wp_start_template_enhancement_output_buffer() ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); ini_set( 'default_mimetype', 'application/json' ); // Since sending a header won't work. @@ -800,11 +800,11 @@ public function test_wp_start_template_optimization_output_buffer_for_json(): vo echo $json; $ob_status = ob_get_status(); - $this->assertSame( 'wp_finalize_template_optimization_output_buffer', $ob_status['name'], 'Expected name to be WP function.' ); + $this->assertSame( 'wp_finalize_template_enhancement_output_buffer', $ob_status['name'], 'Expected name to be WP function.' ); $this->assertSame( 1, $ob_status['type'], 'Expected type to be user supplied handler.' ); $this->assertSame( 0, $ob_status['chunk_size'], 'Expected unlimited chunk size.' ); - ob_end_flush(); // End the buffer started by wp_start_template_optimization_output_buffer(). + ob_end_flush(); // End the buffer started by wp_start_template_enhancement_output_buffer(). $this->assertSame( $initial_ob_level, ob_get_level() ); $this->assertSame( 0, $mock_filter_callback->get_call_count() ); From 16d79283b23668fc81764a694375a204015c653c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 11 Oct 2025 12:34:03 -0700 Subject: [PATCH 26/32] Add wp_should_output_buffer_template_for_enhancement() helper function --- src/wp-includes/template.php | 40 ++++++++++++++++++++++++++------ tests/phpunit/tests/template.php | 10 ++++++-- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 78b03b4678a3e..c68e01346b6da 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -825,17 +825,19 @@ function load_template( $_template_file, $load_once = true, $args = array() ) { } /** - * Starts the template enhancement output buffer. + * Checks whether the template should be output buffered for enhancement. * - * This function is called immediately before the template is included. + * By default, an output buffer is only started if a {@see 'wp_template_enhancement_output_buffer'} filter has been + * added be the time a template is included at the {@see 'wp_before_include_template'} action. This allows template + * responses to be streamed as much as possible when no template enhancements are registered to apply. * * @since 6.9.0 * - * @return bool Whether the output buffer successfully started. + * @return bool Whether the template should be output-buffered for enhancement. */ -function wp_start_template_enhancement_output_buffer(): bool { +function wp_should_output_buffer_template_for_enhancement(): bool { /** - * Filters whether the template will be output-buffered for enhancement. + * Filters whether the template should be output-buffered for enhancement. * * By default, an output buffer is only started if a {@see 'wp_template_enhancement_output_buffer'} filter has been * added. For this default to apply, a filter must be added by the time the template is included at the @@ -847,11 +849,24 @@ function wp_start_template_enhancement_output_buffer(): bool { * * @param bool $use_output_buffer Whether an output buffer is started. */ - if ( ! apply_filters( 'wp_template_output_buffered_for_enhancement', has_filter( 'wp_template_enhancement_output_buffer' ) ) ) { + return (bool) apply_filters( 'wp_should_output_buffer_template_for_enhancement', has_filter( 'wp_template_enhancement_output_buffer' ) ); +} + +/** + * Starts the template enhancement output buffer. + * + * This function is called immediately before the template is included. + * + * @since 6.9.0 + * + * @return bool Whether the output buffer successfully started. + */ +function wp_start_template_enhancement_output_buffer(): bool { + if ( ! wp_should_output_buffer_template_for_enhancement() ) { return false; } - return ob_start( + $started = ob_start( 'wp_finalize_template_enhancement_output_buffer', 0, // Unlimited buffer size so that entire output is passed to the filter. /* @@ -874,6 +889,17 @@ function wp_start_template_enhancement_output_buffer(): bool { */ PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE ); + + if ( $started ) { + /** + * Fires when the template enhancement output buffer has started. + * + * @since 6.9.0 + */ + do_action( 'wp_template_enhancement_output_buffer_started' ); + } + + return $started; } /** diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 1656891e93505..d07407e6ef929 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -505,11 +505,13 @@ public function test_locate_template_uses_current_theme() { * Tests that wp_start_template_enhancement_output_buffer() does not start a buffer when no filters are present. * * @ticket 43258 + * @covers ::wp_should_output_buffer_template_for_enhancement * @covers ::wp_start_template_enhancement_output_buffer */ public function test_wp_start_template_enhancement_output_buffer_without_filters_and_no_override(): void { remove_all_filters( 'wp_template_enhancement_output_buffer' ); $level = ob_get_level(); + $this->assertFalse( wp_should_output_buffer_template_for_enhancement() ); $this->assertFalse( wp_start_template_enhancement_output_buffer() ); $this->assertSame( $level, ob_get_level() ); } @@ -518,12 +520,14 @@ public function test_wp_start_template_enhancement_output_buffer_without_filters * Tests that wp_start_template_enhancement_output_buffer() does start a buffer when no filters are present but there is an override. * * @ticket 43258 + * @covers ::wp_should_output_buffer_template_for_enhancement * @covers ::wp_start_template_enhancement_output_buffer */ public function test_wp_start_template_enhancement_output_buffer_begins_without_filters_but_overridden(): void { remove_all_filters( 'wp_template_enhancement_output_buffer' ); $level = ob_get_level(); - add_filter( 'wp_template_output_buffered_for_enhancement', '__return_true' ); + add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_true' ); + $this->assertTrue( wp_should_output_buffer_template_for_enhancement() ); $this->assertTrue( wp_start_template_enhancement_output_buffer() ); $this->assertSame( $level + 1, ob_get_level() ); ob_end_clean(); @@ -533,6 +537,7 @@ public function test_wp_start_template_enhancement_output_buffer_begins_without_ * Tests that wp_start_template_enhancement_output_buffer() does not start a buffer even when there are filters present due to override. * * @ticket 43258 + * @covers ::wp_should_output_buffer_template_for_enhancement * @covers ::wp_start_template_enhancement_output_buffer */ public function test_wp_start_template_enhancement_output_buffer_begins_with_filters_but_blocked(): void { @@ -543,7 +548,8 @@ static function () { } ); $level = ob_get_level(); - add_filter( 'wp_template_output_buffered_for_enhancement', '__return_false' ); + add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_false' ); + $this->assertFalse( wp_should_output_buffer_template_for_enhancement() ); $this->assertFalse( wp_start_template_enhancement_output_buffer() ); $this->assertSame( $level, ob_get_level() ); } From dfd6e5ec138d46e040ec8848ebd1fefe56250cc7 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 11 Oct 2025 12:44:27 -0700 Subject: [PATCH 27/32] Add assertions for wp_template_enhancement_output_buffer_started action --- tests/phpunit/tests/template.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index d07407e6ef929..489af2685ab00 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -513,6 +513,7 @@ public function test_wp_start_template_enhancement_output_buffer_without_filters $level = ob_get_level(); $this->assertFalse( wp_should_output_buffer_template_for_enhancement() ); $this->assertFalse( wp_start_template_enhancement_output_buffer() ); + $this->assertSame( 0, did_action( 'wp_template_enhancement_output_buffer_started' ) ); $this->assertSame( $level, ob_get_level() ); } @@ -529,6 +530,7 @@ public function test_wp_start_template_enhancement_output_buffer_begins_without_ add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_true' ); $this->assertTrue( wp_should_output_buffer_template_for_enhancement() ); $this->assertTrue( wp_start_template_enhancement_output_buffer() ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ) ); $this->assertSame( $level + 1, ob_get_level() ); ob_end_clean(); } @@ -551,6 +553,7 @@ static function () { add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_false' ); $this->assertFalse( wp_should_output_buffer_template_for_enhancement() ); $this->assertFalse( wp_start_template_enhancement_output_buffer() ); + $this->assertSame( 0, did_action( 'wp_template_enhancement_output_buffer_started' ) ); $this->assertSame( $level, ob_get_level() ); } @@ -597,6 +600,7 @@ static function ( string $buffer ) use ( &$filter_args ): string { $initial_ob_level = ob_get_level(); $this->assertTrue( wp_start_template_enhancement_output_buffer() ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ) ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); ?> @@ -675,6 +679,7 @@ static function ( string $buffer ) use ( &$applied_filter ): string { $initial_ob_level = ob_get_level(); $this->assertTrue( wp_start_template_enhancement_output_buffer() ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ) ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); ?> @@ -738,6 +743,7 @@ static function ( string $buffer ) use ( &$called_html_filter ): string { $initial_ob_level = ob_get_level(); $this->assertTrue( wp_start_template_enhancement_output_buffer() ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ) ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); ?> @@ -791,6 +797,7 @@ public function test_wp_start_template_enhancement_output_buffer_for_json(): voi $initial_ob_level = ob_get_level(); $this->assertTrue( wp_start_template_enhancement_output_buffer() ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ) ); $this->assertSame( $initial_ob_level + 1, ob_get_level() ); ini_set( 'default_mimetype', 'application/json' ); // Since sending a header won't work. From 5409e4421bc2979f7f4134f624cf5ba2fb4bd5f4 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 12 Oct 2025 15:57:59 -0700 Subject: [PATCH 28/32] Remove unnecessary return phpdoc Co-authored-by: Peter Wilson <519727+peterwilsoncc@users.noreply.github.com> --- src/wp-includes/template.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index c68e01346b6da..dfe173d4792d7 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -958,7 +958,6 @@ function wp_finalize_template_enhancement_output_buffer( string $output, int $ph * * @param string $filtered_output HTML template enhancement output buffer. * @param string $output Original HTML template output buffer. - * @return string HTML template enhancement output buffer. */ return (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output ); } From 44d52dfa23c87c031a9952c7b43f80fb2525180e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 12 Oct 2025 15:58:29 -0700 Subject: [PATCH 29/32] Remove unnecessary default params Co-authored-by: Peter Wilson <519727+peterwilsoncc@users.noreply.github.com> --- src/wp-includes/default-filters.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 1a765158a63f2..770091010334c 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -422,7 +422,7 @@ add_action( 'do_all_pings', 'generic_ping', 10, 0 ); add_action( 'do_robots', 'do_robots' ); add_action( 'do_favicon', 'do_favicon' ); -add_action( 'wp_before_include_template', 'wp_start_template_enhancement_output_buffer', 10, 1 ); +add_action( 'wp_before_include_template', 'wp_start_template_enhancement_output_buffer' ); add_action( 'set_comment_cookies', 'wp_set_comment_cookies', 10, 3 ); add_action( 'sanitize_comment_cookies', 'sanitize_comment_cookies' ); add_action( 'init', 'smilies_init', 5 ); From 6bcbf0e2326e9b2ea3ce305001c7858a186089ac Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 12 Oct 2025 16:29:06 -0700 Subject: [PATCH 30/32] Add missing assertion messages Co-authored-by: Peter Wilson <519727+peterwilsoncc@users.noreply.github.com> --- tests/phpunit/tests/template.php | 110 +++++++++++++++---------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 489af2685ab00..676c08129b526 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -511,10 +511,10 @@ public function test_locate_template_uses_current_theme() { public function test_wp_start_template_enhancement_output_buffer_without_filters_and_no_override(): void { remove_all_filters( 'wp_template_enhancement_output_buffer' ); $level = ob_get_level(); - $this->assertFalse( wp_should_output_buffer_template_for_enhancement() ); - $this->assertFalse( wp_start_template_enhancement_output_buffer() ); - $this->assertSame( 0, did_action( 'wp_template_enhancement_output_buffer_started' ) ); - $this->assertSame( $level, ob_get_level() ); + $this->assertFalse( wp_should_output_buffer_template_for_enhancement(), 'Expected wp_should_output_buffer_template_for_enhancement() to return false when there are no wp_template_enhancement_output_buffer filters added.' ); + $this->assertFalse( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return false because the output buffer should not be started.' ); + $this->assertSame( 0, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to not have fired.' ); + $this->assertSame( $level, ob_get_level(), 'Expected the initial output buffer level to be unchanged.' ); } /** @@ -528,10 +528,10 @@ public function test_wp_start_template_enhancement_output_buffer_begins_without_ remove_all_filters( 'wp_template_enhancement_output_buffer' ); $level = ob_get_level(); add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_true' ); - $this->assertTrue( wp_should_output_buffer_template_for_enhancement() ); - $this->assertTrue( wp_start_template_enhancement_output_buffer() ); - $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ) ); - $this->assertSame( $level + 1, ob_get_level() ); + $this->assertTrue( wp_should_output_buffer_template_for_enhancement(), 'Expected wp_should_output_buffer_template_for_enhancement() to return true when overridden with the wp_should_output_buffer_template_for_enhancement filter.' ); + $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true because the output buffer should be started due to the override.' ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' ); + $this->assertSame( $level + 1, ob_get_level(), 'Expected the output buffer level to have been incremented.' ); ob_end_clean(); } @@ -551,10 +551,10 @@ static function () { ); $level = ob_get_level(); add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_false' ); - $this->assertFalse( wp_should_output_buffer_template_for_enhancement() ); - $this->assertFalse( wp_start_template_enhancement_output_buffer() ); - $this->assertSame( 0, did_action( 'wp_template_enhancement_output_buffer_started' ) ); - $this->assertSame( $level, ob_get_level() ); + $this->assertFalse( wp_should_output_buffer_template_for_enhancement(), 'Expected wp_should_output_buffer_template_for_enhancement() to return false since wp_should_output_buffer_template_for_enhancement was filtered to be false even though there is a wp_template_enhancement_output_buffer filter added.' ); + $this->assertFalse( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return false because the output buffer should not be started.' ); + $this->assertSame( 0, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to not have fired.' ); + $this->assertSame( $level, ob_get_level(), 'Expected the initial output buffer level to be unchanged.' ); } /** @@ -599,9 +599,9 @@ static function ( string $buffer ) use ( &$filter_args ): string { ); $initial_ob_level = ob_get_level(); - $this->assertTrue( wp_start_template_enhancement_output_buffer() ); - $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ) ); - $this->assertSame( $initial_ob_level + 1, ob_get_level() ); + $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' ); + $this->assertSame( $initial_ob_level + 1, ob_get_level(), 'Expected the output buffer level to have been incremented' ); ?> @@ -627,7 +627,7 @@ static function ( string $buffer ) use ( &$filter_args ): string { $this->assertSame( 0, $ob_status['chunk_size'], 'Expected unlimited chunk size.' ); ob_end_flush(); // End the buffer started by wp_start_template_enhancement_output_buffer(). - $this->assertSame( $initial_ob_level, ob_get_level() ); + $this->assertSame( $initial_ob_level, ob_get_level(), 'Expected the output buffer to be back at the initial level.' ); $this->assertIsArray( $filter_args, 'Expected the wp_template_enhancement_output_buffer filter to have applied.' ); $this->assertCount( 2, $filter_args, 'Expected two args to be supplied to the wp_template_enhancement_output_buffer filter.' ); @@ -635,21 +635,21 @@ static function ( string $buffer ) use ( &$filter_args ): string { $this->assertIsString( $filter_args[1], 'Expected the $output param to the wp_template_enhancement_output_buffer filter to be a string.' ); $this->assertSame( $filter_args[1], $filter_args[0], 'Expected the initial $filtered_output to match $output in the wp_template_enhancement_output_buffer filter.' ); $original_output = $filter_args[0]; - $this->assertStringContainsString( '', $original_output ); - $this->assertStringContainsString( '', $original_output ); - $this->assertStringContainsString( 'Greeting', $original_output ); - $this->assertStringContainsString( '

Hello World!

', $original_output ); - $this->assertStringContainsString( '', $original_output ); + $this->assertStringContainsString( '', $original_output, 'Expected original output to contain string.' ); + $this->assertStringContainsString( '', $original_output, 'Expected original output to contain string.' ); + $this->assertStringContainsString( 'Greeting', $original_output, 'Expected original output to contain string.' ); + $this->assertStringContainsString( '

Hello World!

', $original_output, 'Expected original output to contain string.' ); + $this->assertStringContainsString( '', $original_output, 'Expected original output to contain string.' ); $processed_output = ob_get_clean(); // Obtain the output via the wrapper output buffer. $this->assertIsString( $processed_output ); $this->assertNotEquals( $original_output, $processed_output ); - $this->assertStringContainsString( '', $processed_output ); - $this->assertStringContainsString( '', $processed_output ); - $this->assertStringContainsString( 'Saludo', $processed_output ); - $this->assertStringContainsString( '

¡Hola, mundo!

', $processed_output ); - $this->assertStringContainsString( '', $processed_output ); + $this->assertStringContainsString( '', $processed_output, 'Expected processed output to contain string.' ); + $this->assertStringContainsString( '', $processed_output, 'Expected processed output to contain string.' ); + $this->assertStringContainsString( 'Saludo', $processed_output, 'Expected processed output to contain string.' ); + $this->assertStringContainsString( '

¡Hola, mundo!

', $processed_output, 'Expected processed output to contain string.' ); + $this->assertStringContainsString( '', $processed_output, 'Expected processed output to contain string.' ); } /** @@ -678,9 +678,9 @@ static function ( string $buffer ) use ( &$applied_filter ): string { ); $initial_ob_level = ob_get_level(); - $this->assertTrue( wp_start_template_enhancement_output_buffer() ); - $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ) ); - $this->assertSame( $initial_ob_level + 1, ob_get_level() ); + $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' ); + $this->assertSame( $initial_ob_level + 1, ob_get_level(), 'Expected the output buffer level to have been incremented' ); ?> @@ -703,17 +703,17 @@ static function ( string $buffer ) use ( &$applied_filter ): string { assertSame( $initial_ob_level, ob_get_level() ); + $this->assertSame( $initial_ob_level, ob_get_level(), 'Expected the output buffer to be back at the initial level.' ); - $this->assertFalse( $applied_filter ); - $this->assertSame( 0, did_action( 'wp_final_template_output_buffer' ) ); + $this->assertFalse( $applied_filter, 'Expected the wp_template_enhancement_output_buffer filter to not have applied.' ); + $this->assertSame( 0, did_action( 'wp_final_template_output_buffer' ), 'Expected the wp_final_template_output_buffer action to not have fired.' ); // Obtain the output via the wrapper output buffer. $output = ob_get_clean(); - $this->assertIsString( $output ); - $this->assertStringNotContainsString( 'Unprocessed', $output ); - $this->assertStringNotContainsString( 'Processed', $output ); - $this->assertStringContainsString( 'Output Buffer Not Processed', $output ); + $this->assertIsString( $output, 'Expected ob_get_clean() to return a string.' ); + $this->assertStringNotContainsString( 'Unprocessed', $output, 'Expected output buffer to not have string since the template was overridden.' ); + $this->assertStringNotContainsString( 'Processed', $output, 'Expected output buffer to not have string since the filter did not apply.' ); + $this->assertStringContainsString( 'Output Buffer Not Processed', $output, 'Expected output buffer to have string since the output buffer was ended with cleaning.' ); } /** @@ -727,11 +727,11 @@ public function test_wp_start_template_enhancement_output_buffer_cleaned_and_rep // Start a wrapper output buffer so that we can flush the inner buffer. ob_start(); - $called_html_filter = false; + $called_filter = false; add_filter( 'wp_template_enhancement_output_buffer', - static function ( string $buffer ) use ( &$called_html_filter ): string { - $called_html_filter = true; + static function ( string $buffer ) use ( &$called_filter ): string { + $called_filter = true; $p = WP_HTML_Processor::create_full_parser( $buffer ); if ( $p->next_tag( array( 'tag_name' => 'TITLE' ) ) ) { @@ -742,9 +742,9 @@ static function ( string $buffer ) use ( &$called_html_filter ): string { ); $initial_ob_level = ob_get_level(); - $this->assertTrue( wp_start_template_enhancement_output_buffer() ); - $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ) ); - $this->assertSame( $initial_ob_level + 1, ob_get_level() ); + $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' ); + $this->assertSame( $initial_ob_level + 1, ob_get_level(), 'Expected the output buffer level to have been incremented.' ); ?> @@ -769,16 +769,16 @@ static function ( string $buffer ) use ( &$called_html_filter ): string { assertSame( $initial_ob_level, ob_get_level() ); + $this->assertSame( $initial_ob_level, ob_get_level(), 'Expected the output buffer to be back at the initial level.' ); - $this->assertTrue( $called_html_filter ); + $this->assertTrue( $called_filter, 'Expected the wp_template_enhancement_output_buffer filter to have applied.' ); // Obtain the output via the wrapper output buffer. $output = ob_get_clean(); - $this->assertIsString( $output ); - $this->assertStringNotContainsString( 'Unprocessed', $output ); - $this->assertStringContainsString( 'Processed', $output ); - $this->assertStringContainsString( '

Template Replaced

', $output ); + $this->assertIsString( $output, 'Expected ob_get_clean() to return a string.' ); + $this->assertStringNotContainsString( 'Unprocessed', $output, 'Expected output buffer to not have string due to template override.' ); + $this->assertStringContainsString( 'Processed', $output, 'Expected output buffer to have string due to filtering.' ); + $this->assertStringContainsString( '

Template Replaced

', $output, 'Expected output buffer to have string due to replaced template.' ); } /** @@ -796,9 +796,9 @@ public function test_wp_start_template_enhancement_output_buffer_for_json(): voi add_filter( 'wp_template_enhancement_output_buffer', array( $mock_filter_callback, 'filter' ) ); $initial_ob_level = ob_get_level(); - $this->assertTrue( wp_start_template_enhancement_output_buffer() ); - $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ) ); - $this->assertSame( $initial_ob_level + 1, ob_get_level() ); + $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' ); + $this->assertSame( $initial_ob_level + 1, ob_get_level(), 'Expected the output buffer level to have been incremented.' ); ini_set( 'default_mimetype', 'application/json' ); // Since sending a header won't work. $json = wp_json_encode( @@ -818,14 +818,14 @@ public function test_wp_start_template_enhancement_output_buffer_for_json(): voi $this->assertSame( 0, $ob_status['chunk_size'], 'Expected unlimited chunk size.' ); ob_end_flush(); // End the buffer started by wp_start_template_enhancement_output_buffer(). - $this->assertSame( $initial_ob_level, ob_get_level() ); + $this->assertSame( $initial_ob_level, ob_get_level(), 'Expected the output buffer to be back at the initial level.' ); - $this->assertSame( 0, $mock_filter_callback->get_call_count() ); + $this->assertSame( 0, $mock_filter_callback->get_call_count(), 'Expected the wp_template_enhancement_output_buffer filter to not have applied.' ); // Obtain the output via the wrapper output buffer. $output = ob_get_clean(); - $this->assertIsString( $output ); - $this->assertSame( $json, $output ); + $this->assertIsString( $output, 'Expected ob_get_clean() to return a string.' ); + $this->assertSame( $json, $output, 'Expected output to not be processed.' ); } public function assertTemplateHierarchy( $url, array $expected, $message = '' ) { From 6333403df2998eaedfda207c821203d85821d73a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 15 Oct 2025 07:34:08 -0700 Subject: [PATCH 31/32] Improve docs for wp_finalize_template_enhancement_output_buffer() Co-authored-by: Aaron Jorbin --- src/wp-includes/template.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index dfe173d4792d7..84ffeb213e838 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -905,6 +905,9 @@ function wp_start_template_enhancement_output_buffer(): bool { /** * Finalizes the template enhancement output buffer. * + * Checks to see if the output buffer is complete and contains HTML. If so, runs the content through + * the `wp_template_enhancement_output_buffer` filter. If not, the original content is returned. + * * @since 6.9.0 * * @see wp_start_template_enhancement_output_buffer() From f49563c7a20f3fb1d3e6115cb20cdc2268cc6b40 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 15 Oct 2025 07:35:19 -0700 Subject: [PATCH 32/32] Remove whitespace at end of line --- src/wp-includes/template.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 84ffeb213e838..65771bf418e42 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -907,7 +907,7 @@ function wp_start_template_enhancement_output_buffer(): bool { * * Checks to see if the output buffer is complete and contains HTML. If so, runs the content through * the `wp_template_enhancement_output_buffer` filter. If not, the original content is returned. - * + * * @since 6.9.0 * * @see wp_start_template_enhancement_output_buffer()