Make WordPress Core

Changeset 61088


Ignore:
Timestamp:
10/30/2025 12:46:29 AM (3 months ago)
Author:
westonruter
Message:

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 https://github.com/WordPress/wordpress-develop/pull/10381

Follow-up to [60936].

Props westonruter, peterwilsoncc, johnbillion.
See #43258.
Fixes #64126.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp.php

    r59728 r61088  
    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         *
  • trunk/src/wp-includes/default-filters.php

    r61078 r61088  
    423423add_action( 'do_robots', 'do_robots' );
    424424add_action( 'do_favicon', 'do_favicon' );
    425 add_action( 'wp_before_include_template', 'wp_start_template_enhancement_output_buffer' );
     425add_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.
    426426add_action( 'set_comment_cookies', 'wp_set_comment_cookies', 10, 3 );
    427427add_action( 'sanitize_comment_cookies', 'sanitize_comment_cookies' );
  • trunk/src/wp-includes/template.php

    r60994 r61088  
    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
     
    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
     
    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    }
     
    978981     * @param string $output          Original HTML template output buffer.
    979982     */
    980     return (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output );
    981 }
     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;
     1004}
  • trunk/tests/phpunit/tests/template.php

    r61076 r61088  
    604604     *
    605605     * @ticket 43258
     606     *
    606607     * @covers ::wp_should_output_buffer_template_for_enhancement
    607608     * @covers ::wp_start_template_enhancement_output_buffer
     
    627628     *
    628629     * @ticket 43258
     630     * @ticket 64126
     631     *
    629632     * @covers ::wp_start_template_enhancement_output_buffer
    630633     * @covers ::wp_finalize_template_enhancement_output_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();
    641 
     642            array( $mock_filter_callback, 'filter' ),
     643            10,
     644            PHP_INT_MAX
     645        );
     646
     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() ) {
     
    657673                }
    658674                return $p->get_updated_html();
    659             },
    660             10,
    661             PHP_INT_MAX
     675            }
    662676        );
    663677
     
    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.' );
     
    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
     
    723745     *
    724746     * @ticket 43258
     747     * @ticket 64126
     748     *
    725749     * @covers ::wp_start_template_enhancement_output_buffer
    726750     * @covers ::wp_finalize_template_enhancement_output_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;
    737 
     759            array( $mock_filter_callback, 'filter' )
     760        );
     761
     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' ) ) ) {
     
    742769                return $p->get_updated_html();
    743770            }
     771        );
     772
     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
    744779        );
    745780
     
    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.
     
    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
     
    790827     *
    791828     * @ticket 43258
     829     * @ticket 64126
     830     *
    792831     * @covers ::wp_start_template_enhancement_output_buffer
    793832     * @covers ::wp_finalize_template_enhancement_output_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;
    804 
     841            array( $mock_filter_callback, 'filter' )
     842        );
     843
     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' ) ) ) {
     
    809851                return $p->get_updated_html();
    810852            }
     853        );
     854
     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
    811861        );
    812862
     
    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.
     
    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
     
    861917     *
    862918     * @ticket 43258
     919     * @ticket 64126
     920     *
    863921     * @covers ::wp_start_template_enhancement_output_buffer
    864922     * @covers ::wp_finalize_template_enhancement_output_buffer
     
    870928        $mock_filter_callback = new MockAction();
    871929        add_filter( 'wp_template_enhancement_output_buffer', array( $mock_filter_callback, 'filter' ) );
     930
     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        );
    872938
    873939        $initial_ob_level = ob_get_level();
     
    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
Note: See TracChangeset for help on using the changeset viewer.