Skip to content

Commit 230e1b8

Browse files
authored
Merge pull request #1807 from b1ink0/add/cache-control-site-health-test
Add Site Health check for `Cache-Control` headers to prevent bfcache from being disabled
2 parents 082bf01 + fe7444b commit 230e1b8

File tree

4 files changed

+293
-0
lines changed

4 files changed

+293
-0
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
/**
3+
* Helper functions used for Cache-Control headers for bfcache compatibility site health check.
4+
*
5+
* @package performance-lab
6+
* @since n.e.x.t
7+
*/
8+
9+
// @codeCoverageIgnoreStart
10+
if ( ! defined( 'ABSPATH' ) ) {
11+
exit; // Exit if accessed directly.
12+
}
13+
// @codeCoverageIgnoreEnd
14+
15+
/**
16+
* Tests the Cache-Control headers for bfcache compatibility.
17+
*
18+
* @since n.e.x.t
19+
* @access private
20+
*
21+
* @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string} Result.
22+
*/
23+
function perflab_bfcache_compatibility_headers_check(): array {
24+
$result = array(
25+
'label' => __( 'The Cache-Control page header is compatible with fast back/forward navigations', 'performance-lab' ),
26+
'status' => 'good',
27+
'badge' => array(
28+
'label' => __( 'Performance', 'performance-lab' ),
29+
'color' => 'blue',
30+
),
31+
'description' => '<p>' . wp_kses(
32+
__( 'If the <code>Cache-Control</code> page response header includes directives like <code>no-store</code>, <code>no-cache</code>, or <code>max-age=0</code> then it can prevent instant back/forward navigations (using the browser bfcache). These are not present for unauthenticated requests on your site, so it is configured properly. Note that WordPress adds these directives for logged-in page responses.', 'performance-lab' ),
33+
array( 'code' => array() )
34+
) . '</p>',
35+
'actions' => '',
36+
'test' => 'perflab_cch_cache_control_header_check',
37+
);
38+
39+
$response = wp_remote_get(
40+
home_url( '/' ),
41+
array(
42+
'headers' => array( 'Accept' => 'text/html' ),
43+
'sslverify' => false,
44+
)
45+
);
46+
47+
if ( is_wp_error( $response ) ) {
48+
$result['label'] = __( 'Unable to check whether the Cache-Control page header is compatible with fast back/forward navigations', 'performance-lab' );
49+
$result['status'] = 'recommended';
50+
$result['description'] = '<p>' . wp_kses(
51+
sprintf(
52+
/* translators: 1: the error code, 2: the error message */
53+
__( 'The unauthenticated request to check the <code>Cache-Control</code> response header for the home page resulted in an error with code <code>%1$s</code> and the following message: %2$s.', 'performance-lab' ),
54+
esc_html( (string) $response->get_error_code() ),
55+
esc_html( rtrim( $response->get_error_message(), '.' ) )
56+
),
57+
array( 'code' => array() )
58+
) . '</p>';
59+
return $result;
60+
}
61+
62+
$cache_control_headers = wp_remote_retrieve_header( $response, 'cache-control' );
63+
if ( '' === $cache_control_headers ) {
64+
// The Cache-Control header is not set, so it does not prevent bfcache. Return the default result.
65+
return $result;
66+
}
67+
68+
foreach ( (array) $cache_control_headers as $cache_control_header ) {
69+
$cache_control_header = strtolower( $cache_control_header );
70+
$found_directives = array();
71+
foreach ( array( 'no-store', 'no-cache', 'max-age=0' ) as $directive ) {
72+
if ( str_contains( $cache_control_header, $directive ) ) {
73+
$found_directives[] = $directive;
74+
}
75+
}
76+
77+
if ( count( $found_directives ) > 0 ) {
78+
$result['label'] = __( 'The Cache-Control page header is preventing fast back/forward navigations', 'performance-lab' );
79+
$result['status'] = 'recommended';
80+
$result['description'] = sprintf(
81+
'<p>%s %s</p>',
82+
wp_kses(
83+
sprintf(
84+
/* translators: %s: problematic directive(s) */
85+
_n(
86+
'The <code>Cache-Control</code> response header for an unauthenticated request to the home page includes the following directive: %s.',
87+
'The <code>Cache-Control</code> response header for an unauthenticated request to the home page includes the following directives: %s.',
88+
count( $found_directives ),
89+
'performance-lab'
90+
),
91+
implode(
92+
', ',
93+
array_map(
94+
static function ( $header ) {
95+
return "<code>$header</code>";
96+
},
97+
$found_directives
98+
)
99+
)
100+
),
101+
array( 'code' => array() )
102+
),
103+
esc_html__( 'This can affect the performance of your site by preventing fast back/forward navigations (via browser bfcache).', 'performance-lab' )
104+
);
105+
break;
106+
}
107+
}
108+
109+
return $result;
110+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
/**
3+
* Hook callbacks used for cache-control headers.
4+
*
5+
* @package performance-lab
6+
* @since n.e.x.t
7+
*/
8+
9+
// @codeCoverageIgnoreStart
10+
if ( ! defined( 'ABSPATH' ) ) {
11+
exit; // Exit if accessed directly.
12+
}
13+
// @codeCoverageIgnoreEnd
14+
15+
/**
16+
* Add the bfcache compatibility check to site health tests.
17+
*
18+
* @since n.e.x.t
19+
* @access private
20+
*
21+
* @param array{direct: array<string, array{label: string, test: string}>} $tests Site Health Tests.
22+
* @return array{direct: array<string, array{label: string, test: string}>} Amended tests.
23+
*/
24+
function perflab_bfcache_compatibility_headers_add_test( array $tests ): array {
25+
$tests['direct']['perflab_bfcache_compatibility_headers'] = array(
26+
'label' => __( 'Cache-Control headers may prevent fast back/forward navigation', 'performance-lab' ),
27+
'test' => 'perflab_bfcache_compatibility_headers_check',
28+
);
29+
return $tests;
30+
}
31+
add_filter( 'site_status_tests', 'perflab_bfcache_compatibility_headers_add_test' );

plugins/performance-lab/includes/site-health/load.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@
3535
// Effective Asset Cache Headers site health check.
3636
require_once __DIR__ . '/effective-asset-cache-headers/helper.php';
3737
require_once __DIR__ . '/effective-asset-cache-headers/hooks.php';
38+
39+
// Cache-Control headers site health check.
40+
require_once __DIR__ . '/bfcache-compatibility-headers/helper.php';
41+
require_once __DIR__ . '/bfcache-compatibility-headers/hooks.php';
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
/**
3+
* Tests for cache-control headers for bfcache compatibility site health check.
4+
*
5+
* @package performance-lab
6+
* @group bfcache-compatibility-headers
7+
*/
8+
9+
class Test_BFCache_Compatibility_Headers extends WP_UnitTestCase {
10+
11+
/**
12+
* Holds mocked response headers for different test scenarios.
13+
*
14+
* @var array<string, array<string, mixed>>
15+
*/
16+
protected $mocked_responses = array();
17+
18+
/**
19+
* Setup each test.
20+
*/
21+
public function setUp(): void {
22+
parent::setUp();
23+
24+
// Clear any filters or mocks.
25+
remove_all_filters( 'pre_http_request' );
26+
27+
// Add the filter to mock HTTP requests.
28+
add_filter( 'pre_http_request', array( $this, 'mock_http_requests' ), 10, 3 );
29+
}
30+
31+
/**
32+
* Test that the bfcache compatibility test is added to the site health tests.
33+
*
34+
* @covers ::perflab_bfcache_compatibility_headers_add_test
35+
*/
36+
public function test_perflab_bfcache_compatibility_headers_add_test(): void {
37+
$tests = array(
38+
'direct' => array(),
39+
);
40+
41+
$tests = perflab_bfcache_compatibility_headers_add_test( $tests );
42+
$this->assertArrayHasKey( 'perflab_bfcache_compatibility_headers', $tests['direct'] );
43+
$this->assertEquals( 'Cache-Control headers may prevent fast back/forward navigation', $tests['direct']['perflab_bfcache_compatibility_headers']['label'] );
44+
$this->assertEquals( 'perflab_bfcache_compatibility_headers_check', $tests['direct']['perflab_bfcache_compatibility_headers']['test'] );
45+
}
46+
47+
/**
48+
* Test that the bfcache compatibility test is attached to the site status tests.
49+
*
50+
* @covers ::perflab_bfcache_compatibility_headers_add_test
51+
*/
52+
public function test_perflab_bfcache_compatibility_headers_add_test_is_attached(): void {
53+
$this->assertNotFalse( has_filter( 'site_status_tests', 'perflab_bfcache_compatibility_headers_add_test' ) );
54+
}
55+
56+
/**
57+
* Test that different Cache-Control headers return the correct bfcache compatibility result.
58+
*
59+
* @dataProvider data_test_bfcache_compatibility
60+
* @covers ::perflab_bfcache_compatibility_headers_check
61+
*
62+
* @param array<int, mixed>|WP_Error $response The response headers.
63+
* @param string $expected_status The expected status.
64+
* @param string $expected_message The expected message.
65+
*/
66+
public function test_perflab_bfcache_compatibility_headers_check( $response, string $expected_status, string $expected_message ): void {
67+
$this->mocked_responses = array( home_url( '/' ) => $response );
68+
69+
$result = perflab_bfcache_compatibility_headers_check();
70+
71+
$this->assertEquals( $expected_status, $result['status'] );
72+
$this->assertStringContainsString( $expected_message, $result['description'] );
73+
}
74+
75+
/**
76+
* Data provider for bfcache compatibility tests.
77+
*
78+
* @return array<string, array<int, mixed>> Test data.
79+
*/
80+
public function data_test_bfcache_compatibility(): array {
81+
return array(
82+
'headers_not_set' => array(
83+
$this->build_response( 200, array( 'cache-control' => '' ) ),
84+
'good',
85+
'If the <code>Cache-Control</code> page response header includes directives like',
86+
),
87+
'no_store' => array(
88+
$this->build_response( 200, array( 'cache-control' => 'no-store' ) ),
89+
'recommended',
90+
'<p>The <code>Cache-Control</code> response header for an unauthenticated request to the home page includes the following directive: <code>no-store</code>',
91+
),
92+
'no_cache' => array(
93+
$this->build_response( 200, array( 'cache-control' => 'no-cache' ) ),
94+
'recommended',
95+
'<p>The <code>Cache-Control</code> response header for an unauthenticated request to the home page includes the following directive: <code>no-cache</code>',
96+
),
97+
'max_age_0' => array(
98+
$this->build_response( 200, array( 'cache-control' => 'max-age=0' ) ),
99+
'recommended',
100+
'<p>The <code>Cache-Control</code> response header for an unauthenticated request to the home page includes the following directive: <code>max-age=0</code>',
101+
),
102+
'max_age_0_no_store' => array(
103+
$this->build_response( 200, array( 'cache-control' => 'max-age=0, no-store' ) ),
104+
'recommended',
105+
'<p>The <code>Cache-Control</code> response header for an unauthenticated request to the home page includes the following directives: <code>no-store</code>, <code>max-age=0</code>',
106+
),
107+
'error' => array(
108+
new WP_Error( 'http_request_failed', 'HTTP request failed' ),
109+
'recommended',
110+
'The unauthenticated request to check the <code>Cache-Control</code> response header for the home page resulted in an error with code',
111+
),
112+
);
113+
}
114+
115+
/**
116+
* Mock HTTP requests for assets to simulate different responses.
117+
*
118+
* @param bool $response A preemptive return value of an HTTP request. Default false.
119+
* @param array<string, mixed> $args Request arguments.
120+
* @param string $url The request URL.
121+
* @return array<string, mixed>|WP_Error Mocked response.
122+
*/
123+
public function mock_http_requests( bool $response, array $args, string $url ) {
124+
if ( isset( $this->mocked_responses[ $url ] ) ) {
125+
return $this->mocked_responses[ $url ];
126+
}
127+
128+
// If no specific mock set, default to a generic success with no caching.
129+
return $this->build_response( 200 );
130+
}
131+
132+
/**
133+
* Helper method to build a mock HTTP response.
134+
*
135+
* @param int $status_code HTTP status code.
136+
* @param array<string, string|int> $headers HTTP headers.
137+
* @return array{response: array{code: int, message: string}, headers: WpOrg\Requests\Utility\CaseInsensitiveDictionary}
138+
*/
139+
protected function build_response( int $status_code = 200, array $headers = array() ): array {
140+
return array(
141+
'response' => array(
142+
'code' => $status_code,
143+
'message' => '',
144+
),
145+
'headers' => new WpOrg\Requests\Utility\CaseInsensitiveDictionary( $headers ),
146+
);
147+
}
148+
}

0 commit comments

Comments
 (0)