Make WordPress Core

Changeset 60936

Timestamp:
10/15/2025 05:12:32 PM (5 weeks ago)
Author:
westonruter
Message:

General: Introduce output buffering for template enhancements.

This introduces an output buffer for the entire template rendering process. This allows for post-processing of the complete HTML output via filtering before it is sent to the browser. This is primarily intended for performance optimizations and other progressive enhancements. Extenders must not rely on output buffer processing for critical content and functionality since a site may opt out of output buffering for the sake of streaming. Extenders are heavily encouraged to use the HTML API as opposed to using regular expressions in output buffer filters.

  • A new wp_before_include_template action is introduced, which fires immediately before the template file is included. This is useful on its own, as it avoids the need to misuse template_include filter to run logic right before the template is loaded (e.g. sending a Server-Timing header).
  • The wp_start_template_enhancement_output_buffer() function is hooked to this new action. It starts an output buffer, but only if there are wp_template_enhancement_output_buffer filters present, or else if there is an explicit opt-in via the wp_should_output_buffer_template_for_enhancement filter.
  • The wp_finalize_template_enhancement_output_buffer() function serves as the output buffer callback. It applies wp_template_enhancement_output_buffer filters to the buffered content if the response is identified as HTML.
  • The output buffer callback passes through (without filtering) any content for non-HTML responses, identified by the Content-Type response header.
  • This provides a standardized way for plugins (and core) to perform optimizations, such as removing unused CSS, without each opening their own ad hoc output buffer.

Developed in https://github.com/WordPress/wordpress-develop/pull/8412.

Props westonruter, nextendweb, dmsnell, flixos90, jorbin, peterwilsoncc, swissspidy, DrewAPicture, DaanvandenBergh, OptimizingMatters, tabrisrp, jonoaldersonwp, SergeyBiryukov.
Fixes #43258.

Location:
trunk
Files:
4 edited

Legend:

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

    r60910 r60936  
    423423add_action( 'do_robots', 'do_robots' );
    424424add_action( 'do_favicon', 'do_favicon' );
     425add_action( 'wp_before_include_template', 'wp_start_template_enhancement_output_buffer' );
    425426add_action( 'set_comment_cookies', 'wp_set_comment_cookies', 10, 3 );
    426427add_action( 'sanitize_comment_cookies', 'sanitize_comment_cookies' );
  • trunk/src/wp-includes/template-loader.php

    r60791 r60936  
    114114    $template = apply_filters( 'template_include', $template );
    115115    if ( $template ) {
     116        /**
     117         * Fires immediately before including the template.
     118         *
     119         * @since 6.9.0
     120         *
     121         * @param string $template The path of the template about to be included.
     122         */
     123        do_action( 'wp_before_include_template', $template );
     124
    116125        include $template;
    117126    } elseif ( current_user_can( 'switch_themes' ) ) {
  • trunk/src/wp-includes/template.php

    r57685 r60936  
    824824    do_action( 'wp_after_load_template', $_template_file, $load_once, $args );
    825825}
     826
     827/**
     828 * Checks whether the template should be output buffered for enhancement.
     829 *
     830 * By default, an output buffer is only started if a {@see 'wp_template_enhancement_output_buffer'} filter has been
     831 * added be the time a template is included at the {@see 'wp_before_include_template'} action. This allows template
     832 * responses to be streamed as much as possible when no template enhancements are registered to apply.
     833 *
     834 * @since 6.9.0
     835 *
     836 * @return bool Whether the template should be output-buffered for enhancement.
     837 */
     838function wp_should_output_buffer_template_for_enhancement(): bool {
     839    /**
     840     * Filters whether the template should be output-buffered for enhancement.
     841     *
     842     * By default, an output buffer is only started if a {@see 'wp_template_enhancement_output_buffer'} filter has been
     843     * added. For this default to apply, a filter must be added by the time the template is included at the
     844     * {@see 'wp_before_include_template'} action. This allows template responses to be streamed as much as possible
     845     * when no template enhancements are registered to apply. This filter allows a site to opt in to adding such
     846     * template enhancement filters during the rendering of the template.
     847     *
     848     * @since 6.9.0
     849     *
     850     * @param bool $use_output_buffer Whether an output buffer is started.
     851     */
     852    return (bool) apply_filters( 'wp_should_output_buffer_template_for_enhancement', has_filter( 'wp_template_enhancement_output_buffer' ) );
     853}
     854
     855/**
     856 * Starts the template enhancement output buffer.
     857 *
     858 * This function is called immediately before the template is included.
     859 *
     860 * @since 6.9.0
     861 *
     862 * @return bool Whether the output buffer successfully started.
     863 */
     864function wp_start_template_enhancement_output_buffer(): bool {
     865    if ( ! wp_should_output_buffer_template_for_enhancement() ) {
     866        return false;
     867    }
     868
     869    $started = ob_start(
     870        'wp_finalize_template_enhancement_output_buffer',
     871        0, // Unlimited buffer size so that entire output is passed to the filter.
     872        /*
     873         * Instead of the default PHP_OUTPUT_HANDLER_STDFLAGS (cleanable, flushable, and removable) being used for
     874         * flags, the PHP_OUTPUT_HANDLER_FLUSHABLE flag must be omitted. If the buffer were flushable, then each time
     875         * that ob_flush() is called, a fragment of the output would be sent into the output buffer callback. This
     876         * output buffer is intended to capture the entire response for processing, as indicated by the chunk size of 0.
     877         * So the buffer does not allow flushing to ensure the entire buffer can be processed, such as for optimizing an
     878         * entire HTML document, where markup in the HEAD may need to be adjusted based on markup that appears late in
     879         * the BODY.
     880         *
     881         * If this ends up being problematic, then PHP_OUTPUT_HANDLER_FLUSHABLE could be added to the $flags and the
     882         * output buffer callback could check if the phase is PHP_OUTPUT_HANDLER_FLUSH and abort any subsequent
     883         * processing while also emitting a _doing_it_wrong().
     884         *
     885         * The output buffer needs to be removable because WordPress calls wp_ob_end_flush_all() and then calls
     886         * wp_cache_close(). If the buffers are not all flushed before wp_cache_close() is closed, then some output buffer
     887         * handlers (e.g. for caching plugins) may fail to be able to store the page output in the object cache.
     888         * See <https://github.com/WordPress/performance/pull/1317#issuecomment-2271955356>.
     889         */
     890        PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE
     891    );
     892
     893    if ( $started ) {
     894        /**
     895         * Fires when the template enhancement output buffer has started.
     896         *
     897         * @since 6.9.0
     898         */
     899        do_action( 'wp_template_enhancement_output_buffer_started' );
     900    }
     901
     902    return $started;
     903}
     904
     905/**
     906 * Finalizes the template enhancement output buffer.
     907 *
     908 * Checks to see if the output buffer is complete and contains HTML. If so, runs the content through
     909 * the `wp_template_enhancement_output_buffer` filter.  If not, the original content is returned.
     910 *
     911 * @since 6.9.0
     912 *
     913 * @see wp_start_template_enhancement_output_buffer()
     914 *
     915 * @param string $output Output buffer.
     916 * @param int    $phase  Phase.
     917 * @return string Finalized output buffer.
     918 */
     919function wp_finalize_template_enhancement_output_buffer( string $output, int $phase ): string {
     920    // When the output is being cleaned (e.g. pending template is replaced with error page), do not send it through the filter.
     921    if ( ( $phase & PHP_OUTPUT_HANDLER_CLEAN ) !== 0 ) {
     922        return $output;
     923    }
     924
     925    // Detect if the response is an HTML content type.
     926    $is_html_content_type = null;
     927    $html_content_types   = array( 'text/html', 'application/xhtml+xml' );
     928    foreach ( headers_list() as $header ) {
     929        $header_parts = preg_split( '/\s*[:;]\s*/', strtolower( $header ) );
     930        if (
     931            is_array( $header_parts ) &&
     932            count( $header_parts ) >= 2 &&
     933            'content-type' === $header_parts[0]
     934        ) {
     935            $is_html_content_type = in_array( $header_parts[1], $html_content_types, true );
     936            break; // PHP only sends the first Content-Type header in the list.
     937        }
     938    }
     939    if ( null === $is_html_content_type ) {
     940        $is_html_content_type = in_array( ini_get( 'default_mimetype' ), $html_content_types, true );
     941    }
     942
     943    // If the content type is not HTML, short-circuit since it is not relevant for enhancement.
     944    if ( ! $is_html_content_type ) {
     945        return $output;
     946    }
     947
     948    $filtered_output = $output;
     949
     950    /**
     951     * Filters the template enhancement output buffer prior to sending to the client.
     952     *
     953     * This filter only applies the HTML output of an included template. This filter is a progressive enhancement
     954     * intended for applications such as optimizing markup to improve frontend page load performance. Sites must not
     955     * depend on this filter applying since they may opt to stream the responses instead. Callbacks for this filter are
     956     * highly discouraged from using regular expressions to do any kind of replacement on the output. Use the HTML API
     957     * (either `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use {@see DOM\HtmlDocument} as of PHP 8.4 which
     958     * fully supports HTML5.
     959     *
     960     * @since 6.9.0
     961     *
     962     * @param string $filtered_output HTML template enhancement output buffer.
     963     * @param string $output          Original HTML template output buffer.
     964     */
     965    return (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output );
     966}
  • trunk/tests/phpunit/tests/template.php

    r58136 r60936  
    6464    }
    6565
     66    /**
     67     * @var string
     68     */
     69    protected $original_default_mimetype;
     70
    6671    public function set_up() {
    6772        parent::set_up();
     73        $this->original_default_mimetype = ini_get( 'default_mimetype' );
    6874        register_post_type(
    6975            'cpt',
     
    8490
    8591    public function tear_down() {
     92        ini_set( 'default_mimetype', $this->original_default_mimetype );
    8693        unregister_post_type( 'cpt' );
    8794        unregister_taxonomy( 'taxo' );
     
    493500        switch_theme( $new_theme->get_stylesheet() );
    494501        $this->assertSame( $new_theme->get_stylesheet_directory() . '/index.php', locate_template( $template_names ), 'Incorrect index template found in theme after switch.' );
     502    }
     503
     504    /**
     505     * Tests that wp_start_template_enhancement_output_buffer() does not start a buffer when no filters are present.
     506     *
     507     * @ticket 43258
     508     * @covers ::wp_should_output_buffer_template_for_enhancement
     509     * @covers ::wp_start_template_enhancement_output_buffer
     510     */
     511    public function test_wp_start_template_enhancement_output_buffer_without_filters_and_no_override(): void {
     512        remove_all_filters( 'wp_template_enhancement_output_buffer' );
     513        $level = ob_get_level();
     514        $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.' );
     515        $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.' );
     516        $this->assertSame( 0, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to not have fired.' );
     517        $this->assertSame( $level, ob_get_level(), 'Expected the initial output buffer level to be unchanged.' );
     518    }
     519
     520    /**
     521     * Tests that wp_start_template_enhancement_output_buffer() does start a buffer when no filters are present but there is an override.
     522     *
     523     * @ticket 43258
     524     * @covers ::wp_should_output_buffer_template_for_enhancement
     525     * @covers ::wp_start_template_enhancement_output_buffer
     526     */
     527    public function test_wp_start_template_enhancement_output_buffer_begins_without_filters_but_overridden(): void {
     528        remove_all_filters( 'wp_template_enhancement_output_buffer' );
     529        $level = ob_get_level();
     530        add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_true' );
     531        $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.' );
     532        $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.' );
     533        $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' );
     534        $this->assertSame( $level + 1, ob_get_level(), 'Expected the output buffer level to have been incremented.' );
     535        ob_end_clean();
     536    }
     537
     538    /**
     539     * Tests that wp_start_template_enhancement_output_buffer() does not start a buffer even when there are filters present due to override.
     540     *
     541     * @ticket 43258
     542     * @covers ::wp_should_output_buffer_template_for_enhancement
     543     * @covers ::wp_start_template_enhancement_output_buffer
     544     */
     545    public function test_wp_start_template_enhancement_output_buffer_begins_with_filters_but_blocked(): void {
     546        add_filter(
     547            'wp_template_enhancement_output_buffer',
     548            static function () {
     549                return '<html>Hey!</html>';
     550            }
     551        );
     552        $level = ob_get_level();
     553        add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_false' );
     554        $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.' );
     555        $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.' );
     556        $this->assertSame( 0, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to not have fired.' );
     557        $this->assertSame( $level, ob_get_level(), 'Expected the initial output buffer level to be unchanged.' );
     558    }
     559
     560    /**
     561     * Tests that wp_start_template_enhancement_output_buffer() starts the expected output buffer and that the expected hooks fire for
     562     * an HTML document and that the response is not incrementally flushable.
     563     *
     564     * @ticket 43258
     565     * @covers ::wp_start_template_enhancement_output_buffer
     566     * @covers ::wp_finalize_template_enhancement_output_buffer
     567     */
     568    public function test_wp_start_template_enhancement_output_buffer_for_html(): void {
     569        // Start a wrapper output buffer so that we can flush the inner buffer.
     570        ob_start();
     571
     572        $filter_args = null;
     573        add_filter(
     574            'wp_template_enhancement_output_buffer',
     575            static function ( string $buffer ) use ( &$filter_args ): string {
     576                $filter_args = func_get_args();
     577
     578                $p = WP_HTML_Processor::create_full_parser( $buffer );
     579                while ( $p->next_tag() ) {
     580                    echo $p->get_tag() . PHP_EOL;
     581                    switch ( $p->get_tag() ) {
     582                        case 'HTML':
     583                            $p->set_attribute( 'lang', 'es' );
     584                            break;
     585                        case 'TITLE':
     586                            $p->set_modifiable_text( 'Saludo' );
     587                            break;
     588                        case 'H1':
     589                            if ( $p->next_token() && '#text' === $p->get_token_name() ) {
     590                                $p->set_modifiable_text( '¡Hola, mundo!' );
     591                            }
     592                            break;
     593                    }
     594                }
     595                return $p->get_updated_html();
     596            },
     597            10,
     598            PHP_INT_MAX
     599        );
     600
     601        $initial_ob_level = ob_get_level();
     602        $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' );
     603        $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' );
     604        $this->assertSame( $initial_ob_level + 1, ob_get_level(), 'Expected the output buffer level to have been incremented' );
     605
     606        ?>
     607        <!DOCTYPE html>
     608        <html lang="en">
     609            <head>
     610                <title>Greeting</title>
     611            </head>
     612            <?php
     613            $this->assertFalse(
     614                @ob_flush(), // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
     615                'Expected output buffer to not be incrementally flushable.'
     616            );
     617            ?>
     618            <body>
     619                <h1>Hello World!</h1>
     620            </body>
     621        </html>
     622        <?php
     623
     624        $ob_status = ob_get_status();
     625        $this->assertSame( 'wp_finalize_template_enhancement_output_buffer', $ob_status['name'], 'Expected name to be WP function.' );
     626        $this->assertSame( 1, $ob_status['type'], 'Expected type to be user supplied handler.' );
     627        $this->assertSame( 0, $ob_status['chunk_size'], 'Expected unlimited chunk size.' );
     628
     629        ob_end_flush(); // End the buffer started by wp_start_template_enhancement_output_buffer().
     630        $this->assertSame( $initial_ob_level, ob_get_level(), 'Expected the output buffer to be back at the initial level.' );
     631
     632        $this->assertIsArray( $filter_args, 'Expected the wp_template_enhancement_output_buffer filter to have applied.' );
     633        $this->assertCount( 2, $filter_args, 'Expected two args to be supplied to the wp_template_enhancement_output_buffer filter.' );
     634        $this->assertIsString( $filter_args[0], 'Expected the $filtered_output param to the wp_template_enhancement_output_buffer filter to be a string.' );
     635        $this->assertIsString( $filter_args[1], 'Expected the $output param to the wp_template_enhancement_output_buffer filter to be a string.' );
     636        $this->assertSame( $filter_args[1], $filter_args[0], 'Expected the initial $filtered_output to match $output in the wp_template_enhancement_output_buffer filter.' );
     637        $original_output = $filter_args[0];
     638        $this->assertStringContainsString( '<!DOCTYPE html>', $original_output, 'Expected original output to contain string.' );
     639        $this->assertStringContainsString( '<html lang="en">', $original_output, 'Expected original output to contain string.' );
     640        $this->assertStringContainsString( '<title>Greeting</title>', $original_output, 'Expected original output to contain string.' );
     641        $this->assertStringContainsString( '<h1>Hello World!</h1>', $original_output, 'Expected original output to contain string.' );
     642        $this->assertStringContainsString( '</html>', $original_output, 'Expected original output to contain string.' );
     643
     644        $processed_output = ob_get_clean(); // Obtain the output via the wrapper output buffer.
     645        $this->assertIsString( $processed_output );
     646        $this->assertNotEquals( $original_output, $processed_output );
     647
     648        $this->assertStringContainsString( '<!DOCTYPE html>', $processed_output, 'Expected processed output to contain string.' );
     649        $this->assertStringContainsString( '<html lang="es">', $processed_output, 'Expected processed output to contain string.' );
     650        $this->assertStringContainsString( '<title>Saludo</title>', $processed_output, 'Expected processed output to contain string.' );
     651        $this->assertStringContainsString( '<h1>¡Hola, mundo!</h1>', $processed_output, 'Expected processed output to contain string.' );
     652        $this->assertStringContainsString( '</html>', $processed_output, 'Expected processed output to contain string.' );
     653    }
     654
     655    /**
     656     * Tests that wp_start_template_enhancement_output_buffer() starts the expected output buffer but ending with cleaning prevents any processing.
     657     *
     658     * @ticket 43258
     659     * @covers ::wp_start_template_enhancement_output_buffer
     660     * @covers ::wp_finalize_template_enhancement_output_buffer
     661     */
     662    public function test_wp_start_template_enhancement_output_buffer_ended_cleaned(): void {
     663        // Start a wrapper output buffer so that we can flush the inner buffer.
     664        ob_start();
     665
     666        $applied_filter = false;
     667        add_filter(
     668            'wp_template_enhancement_output_buffer',
     669            static function ( string $buffer ) use ( &$applied_filter ): string {
     670                $applied_filter = true;
     671
     672                $p = WP_HTML_Processor::create_full_parser( $buffer );
     673                if ( $p->next_tag( array( 'tag_name' => 'TITLE' ) ) ) {
     674                    $p->set_modifiable_text( 'Processed' );
     675                }
     676                return $p->get_updated_html();
     677            }
     678        );
     679
     680        $initial_ob_level = ob_get_level();
     681        $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' );
     682        $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' );
     683        $this->assertSame( $initial_ob_level + 1, ob_get_level(), 'Expected the output buffer level to have been incremented' );
     684
     685        ?>
     686        <!DOCTYPE html>
     687            <html lang="en">
     688            <head>
     689                <title>Unprocessed</title>
     690            </head>
     691            <body>
     692                <h1>Hello World!</h1>
     693                <!-- ... -->
     694        <?php ob_end_clean(); // Clean and end the buffer started by wp_start_template_enhancement_output_buffer(). ?>
     695        <!DOCTYPE html>
     696        <html lang="en">
     697            <head>
     698                <title>Output Buffer Not Processed</title>
     699            </head>
     700            <body>
     701                <h1>Template rendering aborted!!!</h1>
     702            </body>
     703        </html>
     704        <?php
     705
     706        $this->assertSame( $initial_ob_level, ob_get_level(), 'Expected the output buffer to be back at the initial level.' );
     707
     708        $this->assertFalse( $applied_filter, 'Expected the wp_template_enhancement_output_buffer filter to not have applied.' );
     709        $this->assertSame( 0, did_action( 'wp_final_template_output_buffer' ), 'Expected the wp_final_template_output_buffer action to not have fired.' );
     710
     711        // Obtain the output via the wrapper output buffer.
     712        $output = ob_get_clean();
     713        $this->assertIsString( $output, 'Expected ob_get_clean() to return a string.' );
     714        $this->assertStringNotContainsString( '<title>Unprocessed</title>', $output, 'Expected output buffer to not have string since the template was overridden.' );
     715        $this->assertStringNotContainsString( '<title>Processed</title>', $output, 'Expected output buffer to not have string since the filter did not apply.' );
     716        $this->assertStringContainsString( '<title>Output Buffer Not Processed</title>', $output, 'Expected output buffer to have string since the output buffer was ended with cleaning.' );
     717    }
     718
     719    /**
     720     * Tests that wp_start_template_enhancement_output_buffer() starts the expected output buffer and cleaning allows the template to be replaced.
     721     *
     722     * @ticket 43258
     723     * @covers ::wp_start_template_enhancement_output_buffer
     724     * @covers ::wp_finalize_template_enhancement_output_buffer
     725     */
     726    public function test_wp_start_template_enhancement_output_buffer_cleaned_and_replaced(): void {
     727        // Start a wrapper output buffer so that we can flush the inner buffer.
     728        ob_start();
     729
     730        $called_filter = false;
     731        add_filter(
     732            'wp_template_enhancement_output_buffer',
     733            static function ( string $buffer ) use ( &$called_filter ): string {
     734                $called_filter = true;
     735
     736                $p = WP_HTML_Processor::create_full_parser( $buffer );
     737                if ( $p->next_tag( array( 'tag_name' => 'TITLE' ) ) ) {
     738                    $p->set_modifiable_text( 'Processed' );
     739                }
     740                return $p->get_updated_html();
     741            }
     742        );
     743
     744        $initial_ob_level = ob_get_level();
     745        $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' );
     746        $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' );
     747        $this->assertSame( $initial_ob_level + 1, ob_get_level(), 'Expected the output buffer level to have been incremented.' );
     748
     749        ?>
     750        <!DOCTYPE html>
     751            <html lang="en">
     752            <head>
     753                <title>Unprocessed</title>
     754            </head>
     755            <body>
     756                <h1>Hello World!</h1>
     757                <!-- ... -->
     758        <?php ob_clean(); // Clean the buffer started by wp_start_template_enhancement_output_buffer(), allowing the following document to replace the above.. ?>
     759        <!DOCTYPE html>
     760        <html lang="en">
     761            <head>
     762                <title>Template Replaced</title>
     763            </head>
     764            <body>
     765                <h1>Template Replaced</h1>
     766                <p>The original template called <code>ob_clean()</code> which allowed this template to take its place.</p>
     767            </body>
     768        </html>
     769        <?php
     770
     771        ob_end_flush(); // End the buffer started by wp_start_template_enhancement_output_buffer().
     772        $this->assertSame( $initial_ob_level, ob_get_level(), 'Expected the output buffer to be back at the initial level.' );
     773
     774        $this->assertTrue( $called_filter, 'Expected the wp_template_enhancement_output_buffer filter to have applied.' );
     775
     776        // Obtain the output via the wrapper output buffer.
     777        $output = ob_get_clean();
     778        $this->assertIsString( $output, 'Expected ob_get_clean() to return a string.' );
     779        $this->assertStringNotContainsString( '<title>Unprocessed</title>', $output, 'Expected output buffer to not have string due to template override.' );
     780        $this->assertStringContainsString( '<title>Processed</title>', $output, 'Expected output buffer to have string due to filtering.' );
     781        $this->assertStringContainsString( '<h1>Template Replaced</h1>', $output, 'Expected output buffer to have string due to replaced template.' );
     782    }
     783
     784    /**
     785     * Tests that wp_start_template_enhancement_output_buffer() starts the expected output buffer and that the output buffer is not processed.
     786     *
     787     * @ticket 43258
     788     * @covers ::wp_start_template_enhancement_output_buffer
     789     * @covers ::wp_finalize_template_enhancement_output_buffer
     790     */
     791    public function test_wp_start_template_enhancement_output_buffer_for_json(): void {
     792        // Start a wrapper output buffer so that we can flush the inner buffer.
     793        ob_start();
     794
     795        $mock_filter_callback = new MockAction();
     796        add_filter( 'wp_template_enhancement_output_buffer', array( $mock_filter_callback, 'filter' ) );
     797
     798        $initial_ob_level = ob_get_level();
     799        $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' );
     800        $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' );
     801        $this->assertSame( $initial_ob_level + 1, ob_get_level(), 'Expected the output buffer level to have been incremented.' );
     802
     803        ini_set( 'default_mimetype', 'application/json' ); // Since sending a header won't work.
     804        $json = wp_json_encode(
     805            array(
     806                'success' => true,
     807                'data'    => array(
     808                    'message' => 'Hello, world!',
     809                    'fish'    => '<o><', // Something that looks like HTML.
     810                ),
     811            )
     812        );
     813        echo $json;
     814
     815        $ob_status = ob_get_status();
     816        $this->assertSame( 'wp_finalize_template_enhancement_output_buffer', $ob_status['name'], 'Expected name to be WP function.' );
     817        $this->assertSame( 1, $ob_status['type'], 'Expected type to be user supplied handler.' );
     818        $this->assertSame( 0, $ob_status['chunk_size'], 'Expected unlimited chunk size.' );
     819
     820        ob_end_flush(); // End the buffer started by wp_start_template_enhancement_output_buffer().
     821        $this->assertSame( $initial_ob_level, ob_get_level(), 'Expected the output buffer to be back at the initial level.' );
     822
     823        $this->assertSame( 0, $mock_filter_callback->get_call_count(), 'Expected the wp_template_enhancement_output_buffer filter to not have applied.' );
     824
     825        // Obtain the output via the wrapper output buffer.
     826        $output = ob_get_clean();
     827        $this->assertIsString( $output, 'Expected ob_get_clean() to return a string.' );
     828        $this->assertSame( $json, $output, 'Expected output to not be processed.' );
    495829    }
    496830
Note: See TracChangeset for help on using the changeset viewer.