Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions plugins/optimization-detective/docs/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,12 +310,6 @@ add_filter( 'od_maximum_viewport_aspect_ratio', static function (): int {
} );
```

### Filter: `od_template_output_buffer` (default: the HTML response)

Filters the template output buffer before sending it to the client.

This filter is added to implement [\#43258](https://core.trac.wordpress.org/ticket/43258) in WordPress core.

### Filter: `od_url_metric_schema_element_item_additional_properties` (default: empty array)

Filters additional schema properties which should be allowed for an element's item in a URL Metric.
Expand Down
2 changes: 1 addition & 1 deletion plugins/optimization-detective/docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ The implementation involves two phases: detection and optimization. Both of thes
The detection phase involves collecting metrics about the viewport and the elements on the page and then submitting this data to the site’s REST API:

1. Extensions register tag visitors which opt in to measurement for the elements they target for optimization. (As elaborated in a section below, tag visitors are simply callbacks which are invoked for every open tag.)
2. The rendered page is captured in an output buffer (which starts at the `template_include` filter with the highest priority). Output buffering ensures that optimizations can be applied to classic themes, block themes, or even completely custom templating systems (e.g. Timber).
2. The rendered page is captured in WordPress 6.9's template enhancement output buffer (via the `wp_template_enhancement_output_buffer` filter). Output buffering ensures that optimizations can be applied to classic themes, block themes, or even completely custom templating systems (e.g. Timber).
3. The rendered page is loaded into an HTML Tag processor instance ([introduced](https://make.wordpress.org/core/2023/03/07/introducing-the-html-api-in-wordpress-6-2/) in WP 6.2).
4. The open tags are iterated over, and all registered tag visitors are invoked for each open tag, giving them the opportunity to opt in the tag for measurement via the `track_tag()` method on the tag visitor context object. A tag which is opted in for measurement will get a `data-od-xpath` attribute added to it.
5. The detection script module is added to the frontend (`detect.js`).
Expand Down
1 change: 0 additions & 1 deletion plugins/optimization-detective/hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

// @codeCoverageIgnoreStart
add_action( 'init', 'od_initialize_extensions', PHP_INT_MAX );
add_filter( 'template_include', 'od_buffer_output', PHP_INT_MAX );
OD_URL_Metrics_Post_Type::add_hooks();
OD_Storage_Lock::add_hooks();
add_action( 'wp', 'od_maybe_add_template_output_buffer_filter' );
Expand Down
86 changes: 23 additions & 63 deletions plugins/optimization-detective/optimization.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,72 +14,18 @@
}
// @codeCoverageIgnoreEnd

/**
* Starts output buffering at the end of the 'template_include' filter.
*
* This is to implement #43258 in core.
*
* This is a hack that would eventually be replaced with something like this in wp-includes/template-loader.php:
*
* $template = apply_filters( 'template_include', $template );
* + ob_start( 'wp_template_output_buffer_callback' );
* if ( $template ) {
* include $template;
* } elseif ( current_user_can( 'switch_themes' ) ) {
*
* @since 0.1.0
* @access private
* @link https://core.trac.wordpress.org/ticket/43258
*
* @param string|mixed $passthrough Value for the template_include filter which is passed through.
* @return string|mixed Unmodified value of $passthrough.
*/
function od_buffer_output( $passthrough ) {
/*
* Instead of the default PHP_OUTPUT_HANDLER_STDFLAGS (cleanable, flushable, and removable) being used for flags,
* we need to omit PHP_OUTPUT_HANDLER_FLUSHABLE. 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 later
* 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 <https://github.com/WordPress/performance/pull/1317#issuecomment-2271955356>.
*/
$flags = PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE;

ob_start(
static function ( string $output, ?int $phase ): string {
// When the output is being cleaned (e.g. the pending template is replaced with an error page), do not send it through the filter.
if ( ( $phase & PHP_OUTPUT_HANDLER_CLEAN ) !== 0 ) {
return $output;
}

/**
* Filters the template output buffer before sending it to the client.
*
* @since 0.1.0
* @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_template_output_buffer
*
* @param string $output Output buffer.
* @return string Filtered output buffer.
*/
return (string) apply_filters( 'od_template_output_buffer', $output );
},
0, // Unlimited buffer size.
$flags
);
return $passthrough;
}

/**
* Adds template output buffer filter for optimization if eligible.
*
* Since WordPress 6.9, this registers the optimization callback on the {@see 'wp_template_enhancement_output_buffer'}
* filter (Core-43258, see r60936) so that the core-managed template enhancement output buffer is reused instead of
* Optimization Detective starting its own. Registering the filter also opts in to starting the core output buffer
* (see {@see wp_should_output_buffer_template_for_enhancement()}); when the response is not eligible for optimization,
* no filter is added and the response is streamed.
*
* @since 0.1.0
* @since n.e.x.t Registers on the WordPress 6.9 `wp_template_enhancement_output_buffer` filter instead of the
* plugin's own `od_template_output_buffer` filter.
* @access private
*/
function od_maybe_add_template_output_buffer_filter(): void {
Expand All @@ -106,7 +52,21 @@ function_exists( 'perflab_server_timing_use_output_buffer' )
) {
$callback = perflab_wrap_server_timing( $callback, 'optimization-detective', 'exist' );
}
add_filter( 'od_template_output_buffer', $callback );
add_filter( 'wp_template_enhancement_output_buffer', $callback );

// Backward compatibility: continue to apply the deprecated `od_template_output_buffer` filter for any third-party
// callbacks still hooked to it. This only emits a deprecation notice (and only runs) when the filter has listeners.
add_filter(
'wp_template_enhancement_output_buffer',
static function ( string $filtered_output ): string {
return (string) apply_filters_deprecated(
'od_template_output_buffer',
array( $filtered_output ),
'Optimization Detective 1.0.0',
'wp_template_enhancement_output_buffer'
);
}
);
}

/**
Expand Down
1 change: 0 additions & 1 deletion plugins/optimization-detective/tests/test-hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ class Test_OD_Hooks extends WP_UnitTestCase {
*/
public function test_hooks_added(): void {
$this->assertEquals( PHP_INT_MAX, has_action( 'init', 'od_initialize_extensions' ) );
$this->assertEquals( PHP_INT_MAX, has_filter( 'template_include', 'od_buffer_output' ) );

$this->assertEquals( 10, has_filter( 'wp', 'od_maybe_add_template_output_buffer_filter' ) );
$this->assertEquals( 10, has_action( 'wp_head', 'od_render_generator_meta_tag' ) );
Expand Down
140 changes: 35 additions & 105 deletions plugins/optimization-detective/tests/test-optimization.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,109 +41,6 @@ public function tear_down(): void {
parent::tear_down();
}

/**
* Make output is buffered and that it is also filtered.
*
* @covers ::od_buffer_output
*/
public function test_od_buffer_output(): void {
$original = 'Hello World!';
$expected = '¡Hola Mundo!';

// To test, a wrapping output buffer is required because ob_get_clean() does not invoke the output
// buffer callback. See <https://stackoverflow.com/a/61439514/93579>.
ob_start();

$filter_invoked = false;
add_filter(
'od_template_output_buffer',
function ( $buffer ) use ( $original, $expected, &$filter_invoked ) {
$this->assertSame( $original, $buffer );
$filter_invoked = true;
return $expected;
}
);

$original_ob_level = ob_get_level();
$template = sprintf( 'page-%s.php', wp_generate_uuid4() );
$this->assertSame( $template, od_buffer_output( $template ), 'Expected value to be passed through.' );
$this->assertSame( $original_ob_level + 1, ob_get_level(), 'Expected call to ob_start().' );
echo $original;

ob_end_flush(); // Flushing invokes the output buffer callback.

$buffer = ob_get_clean(); // Get the buffer from our wrapper output buffer.
$this->assertSame( $expected, $buffer );
$this->assertTrue( $filter_invoked );
}

/**
* Test that calling ob_flush() will not result in the buffer being processed and that ob_clean() will successfully prevent content from being processed.
*
* @covers ::od_buffer_output
*/
public function test_od_buffer_with_cleaning_and_attempted_flushing(): void {
$template_aborted = 'Before time began!';
$template_start = 'The beginning';
$template_middle = ', the middle';
$template_end = ', and the end!';

// To test, a wrapping output buffer is required because ob_get_clean() does not invoke the output
// buffer callback. See <https://stackoverflow.com/a/61439514/93579>.
$initial_level = ob_get_level();
$this->assertTrue( ob_start() );
$this->assertSame( $initial_level + 1, ob_get_level() );

$filter_count = 0;
add_filter(
'od_template_output_buffer',
function ( $buffer ) use ( $template_start, $template_middle, $template_end, &$filter_count ) {
$filter_count++;
$this->assertSame( $template_start . $template_middle . $template_end, $buffer );
return '<filtered>' . $buffer . '</filtered>';
}
);

od_buffer_output( '' );
$this->assertSame( $initial_level + 2, ob_get_level() );

echo $template_aborted;
$this->assertTrue( ob_clean() ); // By cleaning, the above should never be seen by the filter.

// This is the start of what will end up getting filtered.
echo $template_start;

// Attempt to flush the output, which will fail because the output buffer was opened without the flushable flag.
$this->assertFalse( ob_flush() );

// This will also be sent into the filter.
echo $template_middle;
$this->assertFalse( ob_flush() );
$this->assertSame( $initial_level + 2, ob_get_level() );

// Start a nested output buffer which will also end up getting sent into the filter.
$this->assertTrue( ob_start() );
echo $template_end;
$this->assertSame( $initial_level + 3, ob_get_level() );
$this->assertTrue( ob_flush() );
$this->assertTrue( ob_end_flush() );
$this->assertSame( $initial_level + 2, ob_get_level() );

// Close the output buffer opened by od_buffer_output(). This only works in the unit test because the removable flag was passed.
$this->assertTrue( ob_end_flush() );
$this->assertSame( $initial_level + 1, ob_get_level() );

$buffer = ob_get_clean(); // Get the buffer from our wrapper output buffer and close it.
$this->assertSame( $initial_level, ob_get_level() );

$this->assertSame( 1, $filter_count, 'Expected filter to be called once.' );
$this->assertSame(
'<filtered>' . $template_start . $template_middle . $template_end . '</filtered>',
$buffer,
'Excepted return value of filter to be the resulting value for the buffer.'
);
}

/**
* Data provider.
*
Expand Down Expand Up @@ -237,10 +134,43 @@ public function test_od_maybe_add_template_output_buffer_filter( Closure $set_up

$url = $set_up();
$this->go_to( $url );
remove_all_filters( 'od_template_output_buffer' ); // In case go_to() caused them to be added.
remove_all_filters( 'wp_template_enhancement_output_buffer' ); // In case go_to() caused them to be added.

od_maybe_add_template_output_buffer_filter();
$this->assertSame( $expected_has_filter, has_filter( 'od_template_output_buffer' ) );
$this->assertSame( $expected_has_filter, has_filter( 'wp_template_enhancement_output_buffer' ) );
}

/**
* Test that the deprecated od_template_output_buffer filter is still applied for backward compatibility.
*
* @covers ::od_maybe_add_template_output_buffer_filter
*
* @expectedDeprecated od_template_output_buffer
*/
public function test_od_template_output_buffer_filter_backward_compatibility(): void {
self::factory()->post->create();
$this->go_to( home_url( '/' ) );
remove_all_filters( 'wp_template_enhancement_output_buffer' );
remove_all_filters( 'od_template_output_buffer' );
add_filter( 'od_can_optimize_response', '__return_true' );

od_maybe_add_template_output_buffer_filter();
$this->assertTrue( has_filter( 'wp_template_enhancement_output_buffer' ), 'Expected the core enhancement filter to be added.' );

$invoked = false;
add_filter(
'od_template_output_buffer',
static function ( string $buffer ) use ( &$invoked ): string {
$invoked = true;
return $buffer . '<!-- od deprecated filter applied -->';
}
);

$output = '<html><head></head><body></body></html>';
$filtered = (string) apply_filters( 'wp_template_enhancement_output_buffer', $output, $output );

$this->assertTrue( $invoked, 'Expected the deprecated od_template_output_buffer filter to still be applied.' );
$this->assertStringContainsString( '<!-- od deprecated filter applied -->', $filtered );
}

/**
Expand Down
Loading