diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index b0cf34c756911..44c04175abaf2 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -3468,22 +3468,21 @@ public function is_development_environment() { } /** - * Returns a list of headers and its verification callback to verify if page cache is enabled or not. - * - * Note: key is header name and value could be callable function to verify header value. - * Empty value mean existence of header detect page cache is enabled. + * Returns a mapping from response headers to an optional callback to verify if page cache is enabled or not. * * @since 6.1.0 * - * @return array List of client caching headers and their (optional) verification callbacks. + * @return array Mapping of page caching headers and their (optional) verification callbacks. + * A null value means a simple existence check is used for the header. */ - public function get_page_cache_headers() { + public function get_page_cache_headers(): array { $cache_hit_callback = static function ( $header_value ) { - return str_contains( strtolower( $header_value ), 'hit' ); + return 1 === preg_match( '/(^| |,)HIT(,| |$)/i', $header_value ); }; $cache_headers = array( + // Standard HTTP caching headers. 'cache-control' => static function ( $header_value ) { return (bool) preg_match( '/max-age=[1-9]/', $header_value ); }, @@ -3493,36 +3492,107 @@ public function get_page_cache_headers() { 'age' => static function ( $header_value ) { return is_numeric( $header_value ) && $header_value > 0; }, - 'last-modified' => '', - 'etag' => '', + 'last-modified' => null, + 'etag' => null, + 'via' => null, + + /** + * Custom caching headers. + * + * These do not seem to be actually used by any caching layers. There were first introduced in a Site Health + * test in the AMP plugin. They were copied into the Performance Lab plugin's Site Health test before they + * were merged into core. + * + * @link https://github.com/ampproject/amp-wp/pull/6849 + * @link https://github.com/WordPress/performance/pull/263 + * @link https://core.trac.wordpress.org/changeset/54043 + */ 'x-cache-enabled' => static function ( $header_value ) { - return 'true' === strtolower( $header_value ); + return ( 'true' === strtolower( $header_value ) ); }, 'x-cache-disabled' => static function ( $header_value ) { return ( 'on' !== strtolower( $header_value ) ); }, - 'x-srcache-store-status' => $cache_hit_callback, - 'x-srcache-fetch-status' => $cache_hit_callback, - // Generic caching proxies (Nginx, Varnish, etc.) + /** + * CloudFlare. + * + * @link https://developers.cloudflare.com/cache/concepts/cache-responses/ + */ + 'cf-cache-status' => $cache_hit_callback, + + /** + * Fastly. + * + * @link https://www.fastly.com/documentation/reference/http/http-headers/X-Cache/ + */ 'x-cache' => $cache_hit_callback, - 'x-cache-status' => $cache_hit_callback, + + /** + * LightSpeed. + * + * @link https://docs.litespeedtech.com/lscache/devguide/controls/#x-litespeed-cache + */ 'x-litespeed-cache' => $cache_hit_callback, + + /** + * OpenResty srcache-nginx-module. + * + * The `x-srcache-store-status` header indicates if the response was stored in the cache. + * Valid values include `STORE` and `BYPASS`. + * + * The `x-srcache-fetch-status` header indicates if the response was fetched from the cache. + * Valid values include `HIT`, `MISS`, and `BYPASS`. + * + * @link https://github.com/openresty/srcache-nginx-module + */ + 'x-srcache-store-status' => static function ( $header_value ) { + return 'store' === strtolower( $header_value ); + }, + 'x-srcache-fetch-status' => $cache_hit_callback, + + /** + * Nginx. + * + * @link https://blog.nginx.org/blog/nginx-caching-guide + * @link https://www.inmotionhosting.com/support/website/nginx-cache-management/ + */ + 'x-cache-status' => $cache_hit_callback, 'x-proxy-cache' => $cache_hit_callback, - 'via' => '', - // Cloudflare - 'cf-cache-status' => $cache_hit_callback, + /** + * Varnish Cache. + * + * A header with a single number indicates it was not cached. If there are two numbers (or more), then this + * indicates the response was cached. + * + * @link https://vinyl-cache.org/docs/2.1/faq/http.html + * @link https://www.fastly.com/documentation/reference/http/http-headers/X-Varnish/ + * @link https://www.linuxjournal.com/content/speed-your-web-site-varnish + */ + 'x-varnish' => static function ( $header_value ) { + return 1 === preg_match( '/^\d+ \d+/', $header_value ); + }, ); /** * Filters the list of cache headers supported by core. * + * This list indicates how each of the specified headers will be checked to indicate if a page cache is enabled + * or not. WordPress checks for each of the headers in the returned array. If the callback is provided, it will + * be passed the value for the corresponding header and return a boolean value indicating if the header suggests + * that a cache is active. If the value is `null` for the header, then WordPress will assume that a cache is + * active if the header is present, regardless of its value. + * * @since 6.1.0 * - * @param array $cache_headers Array of supported cache headers. + * @param array $cache_headers Mapping from cache-related HTTP headers to whether they + * indicate if a page cache is enabled for the site. `null` + * indicates caching in the presence of the header; a callback is + * provided the header’s value and should return `true` if it + * implies that a cache is active. */ - return apply_filters( 'site_status_page_cache_supported_cache_headers', $cache_headers ); + return (array) apply_filters( 'site_status_page_cache_supported_cache_headers', $cache_headers ); } /** diff --git a/tests/phpunit/tests/admin/wpSiteHealth.php b/tests/phpunit/tests/admin/wpSiteHealth.php index 12b563cdfe41f..6080b477f54c3 100644 --- a/tests/phpunit/tests/admin/wpSiteHealth.php +++ b/tests/phpunit/tests/admin/wpSiteHealth.php @@ -12,10 +12,8 @@ class Tests_Admin_wpSiteHealth extends WP_UnitTestCase { * An instance of the class to test. * * @since 6.1.0 - * - * @var WP_Site_Health */ - private $instance; + private WP_Site_Health $instance; public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { // Include the `WP_Site_Health` file. @@ -172,7 +170,7 @@ public function data_cron_health_checks() { * @covers ::get_page_cache_headers() * @covers ::check_for_page_caching() */ - public function test_get_page_cache( $responses, $expected_status, $expected_label, $good_basic_auth = null, $delay_the_response = false ) { + public function test_get_page_cache( array $responses, string $expected_status, string $expected_label, bool $has_basic_auth = false, bool $delay_the_response = false ) { $expected_props = array( 'badge' => array( 'label' => __( 'Performance' ), @@ -183,7 +181,7 @@ public function test_get_page_cache( $responses, $expected_status, $expected_lab 'label' => $expected_label, ); - if ( null !== $good_basic_auth ) { + if ( $has_basic_auth ) { $_SERVER['PHP_AUTH_USER'] = 'admin'; $_SERVER['PHP_AUTH_PW'] = 'password'; } @@ -200,7 +198,7 @@ static function () use ( $threshold ) { add_filter( 'pre_http_request', - function ( $response, $parsed_args ) use ( &$responses, &$is_unauthorized, $good_basic_auth, $delay_the_response, $threshold ) { + function ( $response, $parsed_args ) use ( &$responses, &$is_unauthorized, $has_basic_auth, $delay_the_response, $threshold ) { $expected_response = array_shift( $responses ); @@ -219,7 +217,7 @@ function ( $response, $parsed_args ) use ( &$responses, &$is_unauthorized, $good ); } - if ( null !== $good_basic_auth ) { + if ( $has_basic_auth ) { $this->assertArrayHasKey( 'Authorization', $parsed_args['headers'] @@ -263,9 +261,15 @@ function ( $response, $parsed_args ) use ( &$responses, &$is_unauthorized, $good * * @ticket 56041 * - * @return array[] + * @return array>, + * expected_status: 'recommended'|'critical'|'good', + * expected_label: string, + * good_basic_auth?: bool, + * delay_the_response?: bool, + * }> */ - public function data_get_page_cache() { + public function data_get_page_cache(): array { $recommended_label = 'Page cache is not detected but the server response time is OK'; $good_label = 'Page cache is detected and the server response time is good'; $critical_label = 'Page cache is not detected and the server response time is slow'; @@ -278,13 +282,13 @@ public function data_get_page_cache() { ), 'expected_status' => 'recommended', 'expected_label' => $error_label, - 'good_basic_auth' => false, + 'has_basic_auth' => true, ), 'no-cache-control' => array( 'responses' => array_fill( 0, 3, array() ), 'expected_status' => 'critical', 'expected_label' => $critical_label, - 'good_basic_auth' => null, + 'has_basic_auth' => false, 'delay_the_response' => true, ), 'no-cache' => array( @@ -310,7 +314,7 @@ public function data_get_page_cache() { 'responses' => array_fill( 0, 3, array( 'cache-control' => 'no-cache' ) ), 'expected_status' => 'critical', 'expected_label' => $critical_label, - 'good_basic_auth' => null, + 'has_basic_auth' => false, 'delay_the_response' => true, ), 'age' => array( @@ -366,7 +370,7 @@ public function data_get_page_cache() { ), 'expected_status' => 'critical', 'expected_label' => $critical_label, - 'good_basic_auth' => null, + 'has_basic_auth' => false, 'delay_the_response' => true, ), 'cache-control-with-basic-auth' => array( @@ -377,7 +381,7 @@ public function data_get_page_cache() { ), 'expected_status' => 'good', 'expected_label' => $good_label, - 'good_basic_auth' => true, + 'has_basic_auth' => true, ), 'x-cache-enabled' => array( 'responses' => array_fill( @@ -396,7 +400,7 @@ public function data_get_page_cache() { ), 'expected_status' => 'critical', 'expected_label' => $critical_label, - 'good_basic_auth' => null, + 'has_basic_auth' => false, 'delay_the_response' => true, ), 'x-cache-disabled' => array( @@ -408,6 +412,78 @@ public function data_get_page_cache() { 'expected_status' => 'good', 'expected_label' => $good_label, ), + 'false-positive-hit-in-word' => array( + 'responses' => array_fill( + 0, + 3, + array( 'x-cache' => 'no-hit' ) + ), + 'expected_status' => 'recommended', + 'expected_label' => $recommended_label, + ), + 'varnish-header' => array( + 'responses' => array_fill( + 0, + 3, + array( 'x-varnish' => '123 456' ) + ), + 'expected_status' => 'good', + 'expected_label' => $good_label, + ), + 'varnish-header-miss' => array( + 'responses' => array_fill( + 0, + 3, + array( 'x-varnish' => '123' ) + ), + 'expected_status' => 'recommended', + 'expected_label' => $recommended_label, + ), + 'srcache-store-status' => array( + 'responses' => array_fill( + 0, + 3, + array( 'x-srcache-store-status' => 'STORE' ) + ), + 'expected_status' => 'good', + 'expected_label' => $good_label, + ), + 'srcache-store-status-bypass' => array( + 'responses' => array_fill( + 0, + 3, + array( 'x-srcache-store-status' => 'BYPASS' ) + ), + 'expected_status' => 'recommended', + 'expected_label' => $recommended_label, + ), + 'srcache-fetch-status' => array( + 'responses' => array_fill( + 0, + 3, + array( 'x-srcache-fetch-status' => 'HIT' ) + ), + 'expected_status' => 'good', + 'expected_label' => $good_label, + ), + 'last-modified' => array( + 'responses' => array_fill( + 0, + 3, + array( 'last-modified' => 'Wed, 21 Oct 2015 07:28:00 GMT' ) + ), + 'expected_status' => 'good', + 'expected_label' => $good_label, + ), + 'via' => array( + 'responses' => array_fill( + 0, + 3, + array( 'via' => '1.1 varnish' ) + ), + 'expected_status' => 'good', + 'expected_label' => $good_label, + ), ); }