Skip to content

Commit b482707

Browse files
committed
General: Add wp_send_late_headers action which fires right before the template enhancement output buffer is flushed.
This adds a (missing) `wp_send_late_headers` action which fires right after the `wp_template_enhancement_output_buffer` filters have applied and right before the output buffer is flushed. The filtered output buffer is passed as an argument to the action so that plugins may do things like send an `ETag` header which is calculated from the content. This action eliminates the need for plugins to hack the `wp_template_enhancement_output_buffer` filter with a high priority to send a late response header. This action compliments the `send_headers` action which is commonly used to send HTTP headers before the template is rendered. Furthermore: * The template enhancement output buffer is now enabled by default if there is a callback added to either the `wp_template_enhancement_output_buffer` filter or the `wp_send_late_headers` action. * The `wp_start_template_enhancement_output_buffer()` callback for the `wp_before_include_template` action is increased from the default of 10 to 1000. This goes with the previous point, so that plugins can add those filters and actions during the `wp_before_include_template` action without having to worry about adding them too late, that is, after `wp_start_template_enhancement_output_buffer()` has run. * The `wp_send_late_headers` action fires regardless of whether the buffered response is HTML. Developed in #10381 Follow-up to [60936]. Props westonruter, peterwilsoncc, johnbillion. See #43258. Fixes #64126. git-svn-id: https://develop.svn.wordpress.org/trunk@61088 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 87cbbb1 commit b482707

File tree

4 files changed

+120
-22
lines changed

4 files changed

+120
-22
lines changed

src/wp-includes/class-wp.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,9 @@ public function send_headers() {
588588
/**
589589
* Fires once the requested HTTP headers for caching, content type, etc. have been sent.
590590
*
591+
* The {@see 'wp_send_late_headers'} action may be used to send headers after rendering the template into an
592+
* output buffer.
593+
*
591594
* @since 2.1.0
592595
*
593596
* @param WP $wp Current WordPress environment instance (passed by reference).

src/wp-includes/default-filters.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@
422422
add_action( 'do_all_pings', 'generic_ping', 10, 0 );
423423
add_action( 'do_robots', 'do_robots' );
424424
add_action( 'do_favicon', 'do_favicon' );
425-
add_action( 'wp_before_include_template', 'wp_start_template_enhancement_output_buffer' );
425+
add_action( 'wp_before_include_template', 'wp_start_template_enhancement_output_buffer', 1000 ); // Late priority to let `wp_template_enhancement_output_buffer` filters and `wp_send_late_headers` actions be registered.
426426
add_action( 'set_comment_cookies', 'wp_set_comment_cookies', 10, 3 );
427427
add_action( 'sanitize_comment_cookies', 'sanitize_comment_cookies' );
428428
add_action( 'init', 'smilies_init', 5 );

src/wp-includes/template.php

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -844,16 +844,17 @@ function wp_should_output_buffer_template_for_enhancement(): bool {
844844
* Filters whether the template should be output-buffered for enhancement.
845845
*
846846
* By default, an output buffer is only started if a {@see 'wp_template_enhancement_output_buffer'} filter has been
847-
* added. For this default to apply, a filter must be added by the time the template is included at the
848-
* {@see 'wp_before_include_template'} action. This allows template responses to be streamed as much as possible
849-
* when no template enhancements are registered to apply. This filter allows a site to opt in to adding such
850-
* template enhancement filters during the rendering of the template.
847+
* added or if a plugin has added a {@see 'wp_send_late_headers'} action. For this default to apply, either of the
848+
* hooks must be added by the time the template is included at the {@see 'wp_before_include_template'} action. This
849+
* allows template responses to be streamed unless the there is code which depends on an output buffer being opened.
850+
* This filter allows a site to opt in to adding such template enhancement filters later during the rendering of the
851+
* template.
851852
*
852853
* @since 6.9.0
853854
*
854855
* @param bool $use_output_buffer Whether an output buffer is started.
855856
*/
856-
return (bool) apply_filters( 'wp_should_output_buffer_template_for_enhancement', has_filter( 'wp_template_enhancement_output_buffer' ) );
857+
return (bool) apply_filters( 'wp_should_output_buffer_template_for_enhancement', has_filter( 'wp_template_enhancement_output_buffer' ) || has_action( 'wp_send_late_headers' ) );
857858
}
858859

859860
/**
@@ -957,6 +958,8 @@ function wp_finalize_template_enhancement_output_buffer( string $output, int $ph
957958

958959
// If the content type is not HTML, short-circuit since it is not relevant for enhancement.
959960
if ( ! $is_html_content_type ) {
961+
/** This action is documented in wp-includes/template.php */
962+
do_action( 'wp_send_late_headers', $output );
960963
return $output;
961964
}
962965

@@ -977,5 +980,25 @@ function wp_finalize_template_enhancement_output_buffer( string $output, int $ph
977980
* @param string $filtered_output HTML template enhancement output buffer.
978981
* @param string $output Original HTML template output buffer.
979982
*/
980-
return (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output );
983+
$filtered_output = (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output );
984+
985+
/**
986+
* Fires at the last moment HTTP headers may be sent.
987+
*
988+
* This happens immediately before the template enhancement output buffer is flushed. This is in contrast with
989+
* the {@see 'send_headers'} action which fires after the initial headers have been sent before the template
990+
* has begun rendering, and thus does not depend on output buffering. This action does not fire if the "template
991+
* enhancement output buffer" was not started. This output buffer is automatically started if this action is added
992+
* before {@see wp_start_template_enhancement_output_buffer()} runs at the {@see 'wp_before_include_template'}
993+
* action with priority 1000. Before this point, the output buffer will also be started automatically if there was a
994+
* {@see 'wp_template_enhancement_output_buffer'} filter added, or if the
995+
* {@see 'wp_should_output_buffer_template_for_enhancement'} filter is made to return `true`.
996+
*
997+
* @since 6.9.0
998+
*
999+
* @param string $output Output buffer.
1000+
*/
1001+
do_action( 'wp_send_late_headers', $filtered_output );
1002+
1003+
return $filtered_output;
9811004
}

tests/phpunit/tests/template.php

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,7 @@ public function test_wp_start_template_enhancement_output_buffer_begins_without_
603603
* Tests that wp_start_template_enhancement_output_buffer() does not start a buffer even when there are filters present due to override.
604604
*
605605
* @ticket 43258
606+
*
606607
* @covers ::wp_should_output_buffer_template_for_enhancement
607608
* @covers ::wp_start_template_enhancement_output_buffer
608609
*/
@@ -626,19 +627,34 @@ static function () {
626627
* an HTML document and that the response is not incrementally flushable.
627628
*
628629
* @ticket 43258
630+
* @ticket 64126
631+
*
629632
* @covers ::wp_start_template_enhancement_output_buffer
630633
* @covers ::wp_finalize_template_enhancement_output_buffer
631634
*/
632635
public function test_wp_start_template_enhancement_output_buffer_for_html(): void {
633636
// Start a wrapper output buffer so that we can flush the inner buffer.
634637
ob_start();
635638

636-
$filter_args = null;
639+
$mock_filter_callback = new MockAction();
637640
add_filter(
638641
'wp_template_enhancement_output_buffer',
639-
static function ( string $buffer ) use ( &$filter_args ): string {
640-
$filter_args = func_get_args();
642+
array( $mock_filter_callback, 'filter' ),
643+
10,
644+
PHP_INT_MAX
645+
);
641646

647+
$mock_action_callback = new MockAction();
648+
add_filter(
649+
'wp_send_late_headers',
650+
array( $mock_action_callback, 'action' ),
651+
10,
652+
PHP_INT_MAX
653+
);
654+
655+
add_filter(
656+
'wp_template_enhancement_output_buffer',
657+
static function ( string $buffer ): string {
642658
$p = WP_HTML_Processor::create_full_parser( $buffer );
643659
while ( $p->next_tag() ) {
644660
switch ( $p->get_tag() ) {
@@ -656,9 +672,7 @@ static function ( string $buffer ) use ( &$filter_args ): string {
656672
}
657673
}
658674
return $p->get_updated_html();
659-
},
660-
10,
661-
PHP_INT_MAX
675+
}
662676
);
663677

664678
$this->assertCount( 0, headers_list(), 'Expected no headers to have been sent during unit tests.' );
@@ -695,6 +709,8 @@ static function ( string $buffer ) use ( &$filter_args ): string {
695709
ob_end_flush(); // End the buffer started by wp_start_template_enhancement_output_buffer().
696710
$this->assertSame( $initial_ob_level, ob_get_level(), 'Expected the output buffer to be back at the initial level.' );
697711

712+
$this->assertSame( 1, $mock_filter_callback->get_call_count(), 'Expected the wp_template_enhancement_output_buffer filter to have applied.' );
713+
$filter_args = $mock_filter_callback->get_args()[0];
698714
$this->assertIsArray( $filter_args, 'Expected the wp_template_enhancement_output_buffer filter to have applied.' );
699715
$this->assertCount( 2, $filter_args, 'Expected two args to be supplied to the wp_template_enhancement_output_buffer filter.' );
700716
$this->assertIsString( $filter_args[0], 'Expected the $filtered_output param to the wp_template_enhancement_output_buffer filter to be a string.' );
@@ -716,25 +732,36 @@ static function ( string $buffer ) use ( &$filter_args ): string {
716732
$this->assertStringContainsString( '<title>Saludo</title>', $processed_output, 'Expected processed output to contain string.' );
717733
$this->assertStringContainsString( '<h1>¡Hola, mundo!</h1>', $processed_output, 'Expected processed output to contain string.' );
718734
$this->assertStringContainsString( '</html>', $processed_output, 'Expected processed output to contain string.' );
735+
736+
$this->assertSame( 1, did_action( 'wp_send_late_headers' ), 'Expected the wp_send_late_headers action to have fired.' );
737+
$this->assertSame( 1, $mock_action_callback->get_call_count(), 'Expected wp_send_late_headers action callback to have been called once.' );
738+
$action_args = $mock_action_callback->get_args()[0];
739+
$this->assertCount( 1, $action_args, 'Expected the wp_send_late_headers action to have been passed only one argument.' );
740+
$this->assertSame( $processed_output, $action_args[0], 'Expected the arg passed to wp_send_late_headers to be the same as the processed output buffer.' );
719741
}
720742

721743
/**
722744
* Tests that wp_start_template_enhancement_output_buffer() starts the expected output buffer but ending with cleaning prevents any processing.
723745
*
724746
* @ticket 43258
747+
* @ticket 64126
748+
*
725749
* @covers ::wp_start_template_enhancement_output_buffer
726750
* @covers ::wp_finalize_template_enhancement_output_buffer
727751
*/
728752
public function test_wp_start_template_enhancement_output_buffer_ended_cleaned(): void {
729753
// Start a wrapper output buffer so that we can flush the inner buffer.
730754
ob_start();
731755

732-
$applied_filter = false;
756+
$mock_filter_callback = new MockAction();
733757
add_filter(
734758
'wp_template_enhancement_output_buffer',
735-
static function ( string $buffer ) use ( &$applied_filter ): string {
736-
$applied_filter = true;
759+
array( $mock_filter_callback, 'filter' )
760+
);
737761

762+
add_filter(
763+
'wp_template_enhancement_output_buffer',
764+
static function ( string $buffer ): string {
738765
$p = WP_HTML_Processor::create_full_parser( $buffer );
739766
if ( $p->next_tag( array( 'tag_name' => 'TITLE' ) ) ) {
740767
$p->set_modifiable_text( 'Processed' );
@@ -743,6 +770,14 @@ static function ( string $buffer ) use ( &$applied_filter ): string {
743770
}
744771
);
745772

773+
$mock_action_callback = new MockAction();
774+
add_filter(
775+
'wp_send_late_headers',
776+
array( $mock_action_callback, 'action' ),
777+
10,
778+
PHP_INT_MAX
779+
);
780+
746781
$this->assertCount( 0, headers_list(), 'Expected no headers to have been sent during unit tests.' );
747782
ini_set( 'default_mimetype', 'text/html' ); // Since sending a header won't work.
748783

@@ -774,34 +809,41 @@ static function ( string $buffer ) use ( &$applied_filter ): string {
774809

775810
$this->assertSame( $initial_ob_level, ob_get_level(), 'Expected the output buffer to be back at the initial level.' );
776811

777-
$this->assertFalse( $applied_filter, 'Expected the wp_template_enhancement_output_buffer filter to not have applied.' );
778-
$this->assertSame( 0, did_action( 'wp_final_template_output_buffer' ), 'Expected the wp_final_template_output_buffer action to not have fired.' );
812+
$this->assertSame( 0, $mock_filter_callback->get_call_count(), 'Expected the wp_template_enhancement_output_buffer filter to not have applied.' );
779813

780814
// Obtain the output via the wrapper output buffer.
781815
$output = ob_get_clean();
782816
$this->assertIsString( $output, 'Expected ob_get_clean() to return a string.' );
783817
$this->assertStringNotContainsString( '<title>Unprocessed</title>', $output, 'Expected output buffer to not have string since the template was overridden.' );
784818
$this->assertStringNotContainsString( '<title>Processed</title>', $output, 'Expected output buffer to not have string since the filter did not apply.' );
785819
$this->assertStringContainsString( '<title>Output Buffer Not Processed</title>', $output, 'Expected output buffer to have string since the output buffer was ended with cleaning.' );
820+
821+
$this->assertSame( 0, did_action( 'wp_send_late_headers' ), 'Expected the wp_send_late_headers action to not have fired.' );
822+
$this->assertSame( 0, $mock_action_callback->get_call_count(), 'Expected wp_send_late_headers action callback to have been called once.' );
786823
}
787824

788825
/**
789826
* Tests that wp_start_template_enhancement_output_buffer() starts the expected output buffer and cleaning allows the template to be replaced.
790827
*
791828
* @ticket 43258
829+
* @ticket 64126
830+
*
792831
* @covers ::wp_start_template_enhancement_output_buffer
793832
* @covers ::wp_finalize_template_enhancement_output_buffer
794833
*/
795834
public function test_wp_start_template_enhancement_output_buffer_cleaned_and_replaced(): void {
796835
// Start a wrapper output buffer so that we can flush the inner buffer.
797836
ob_start();
798837

799-
$called_filter = false;
838+
$mock_filter_callback = new MockAction();
800839
add_filter(
801840
'wp_template_enhancement_output_buffer',
802-
static function ( string $buffer ) use ( &$called_filter ): string {
803-
$called_filter = true;
841+
array( $mock_filter_callback, 'filter' )
842+
);
804843

844+
add_filter(
845+
'wp_template_enhancement_output_buffer',
846+
static function ( string $buffer ): string {
805847
$p = WP_HTML_Processor::create_full_parser( $buffer );
806848
if ( $p->next_tag( array( 'tag_name' => 'TITLE' ) ) ) {
807849
$p->set_modifiable_text( 'Processed' );
@@ -810,6 +852,14 @@ static function ( string $buffer ) use ( &$called_filter ): string {
810852
}
811853
);
812854

855+
$mock_action_callback = new MockAction();
856+
add_filter(
857+
'wp_send_late_headers',
858+
array( $mock_action_callback, 'action' ),
859+
10,
860+
PHP_INT_MAX
861+
);
862+
813863
$this->assertCount( 0, headers_list(), 'Expected no headers to have been sent during unit tests.' );
814864
ini_set( 'default_mimetype', 'application/xhtml+xml' ); // Since sending a header won't work.
815865

@@ -846,20 +896,28 @@ static function ( string $buffer ) use ( &$called_filter ): string {
846896
ob_end_flush(); // End the buffer started by wp_start_template_enhancement_output_buffer().
847897
$this->assertSame( $initial_ob_level, ob_get_level(), 'Expected the output buffer to be back at the initial level.' );
848898

849-
$this->assertTrue( $called_filter, 'Expected the wp_template_enhancement_output_buffer filter to have applied.' );
899+
$this->assertSame( 1, $mock_filter_callback->get_call_count(), 'Expected the wp_template_enhancement_output_buffer filter to have applied.' );
850900

851901
// Obtain the output via the wrapper output buffer.
852902
$output = ob_get_clean();
853903
$this->assertIsString( $output, 'Expected ob_get_clean() to return a string.' );
854904
$this->assertStringNotContainsString( '<title>Unprocessed</title>', $output, 'Expected output buffer to not have string due to template override.' );
855905
$this->assertStringContainsString( '<title>Processed</title>', $output, 'Expected output buffer to have string due to filtering.' );
856906
$this->assertStringContainsString( '<h1>Template Replaced</h1>', $output, 'Expected output buffer to have string due to replaced template.' );
907+
908+
$this->assertSame( 1, did_action( 'wp_send_late_headers' ), 'Expected the wp_send_late_headers action to have fired.' );
909+
$this->assertSame( 1, $mock_action_callback->get_call_count(), 'Expected wp_send_late_headers action callback to have been called once.' );
910+
$action_args = $mock_action_callback->get_args()[0];
911+
$this->assertCount( 1, $action_args, 'Expected the wp_send_late_headers action to have been passed only one argument.' );
912+
$this->assertSame( $output, $action_args[0], 'Expected the arg passed to wp_send_late_headers to be the same as the processed output buffer.' );
857913
}
858914

859915
/**
860916
* Tests that wp_start_template_enhancement_output_buffer() starts the expected output buffer and that the output buffer is not processed.
861917
*
862918
* @ticket 43258
919+
* @ticket 64126
920+
*
863921
* @covers ::wp_start_template_enhancement_output_buffer
864922
* @covers ::wp_finalize_template_enhancement_output_buffer
865923
*/
@@ -870,6 +928,14 @@ public function test_wp_start_template_enhancement_output_buffer_for_json(): voi
870928
$mock_filter_callback = new MockAction();
871929
add_filter( 'wp_template_enhancement_output_buffer', array( $mock_filter_callback, 'filter' ) );
872930

931+
$mock_action_callback = new MockAction();
932+
add_filter(
933+
'wp_send_late_headers',
934+
array( $mock_action_callback, 'action' ),
935+
10,
936+
PHP_INT_MAX
937+
);
938+
873939
$initial_ob_level = ob_get_level();
874940
$this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' );
875941
$this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' );
@@ -903,6 +969,12 @@ public function test_wp_start_template_enhancement_output_buffer_for_json(): voi
903969
$output = ob_get_clean();
904970
$this->assertIsString( $output, 'Expected ob_get_clean() to return a string.' );
905971
$this->assertSame( $json, $output, 'Expected output to not be processed.' );
972+
973+
$this->assertSame( 1, did_action( 'wp_send_late_headers' ), 'Expected the wp_send_late_headers action to have fired even though the wp_template_enhancement_output_buffer filter did not apply.' );
974+
$this->assertSame( 1, $mock_action_callback->get_call_count(), 'Expected wp_send_late_headers action callback to have been called once.' );
975+
$action_args = $mock_action_callback->get_args()[0];
976+
$this->assertCount( 1, $action_args, 'Expected the wp_send_late_headers action to have been passed only one argument.' );
977+
$this->assertSame( $output, $action_args[0], 'Expected the arg passed to wp_send_late_headers to be the same as the processed output buffer.' );
906978
}
907979

908980
/**

0 commit comments

Comments
 (0)