From dc9e36439a3c75e181b6bdb1548e46c95658e4c1 Mon Sep 17 00:00:00 2001 From: b1ink0 Date: Mon, 9 Dec 2024 18:05:17 +0530 Subject: [PATCH 01/12] Add site health checks for far-future expiration headers --- .../far-future-headers/assets/test.css | 0 .../site-health/far-future-headers/helper.php | 115 ++++++++++++++++++ .../site-health/far-future-headers/hooks.php | 28 +++++ .../includes/site-health/load.php | 4 + 4 files changed, 147 insertions(+) create mode 100644 plugins/performance-lab/includes/site-health/far-future-headers/assets/test.css create mode 100644 plugins/performance-lab/includes/site-health/far-future-headers/helper.php create mode 100644 plugins/performance-lab/includes/site-health/far-future-headers/hooks.php diff --git a/plugins/performance-lab/includes/site-health/far-future-headers/assets/test.css b/plugins/performance-lab/includes/site-health/far-future-headers/assets/test.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/performance-lab/includes/site-health/far-future-headers/helper.php b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php new file mode 100644 index 0000000000..180c51cb53 --- /dev/null +++ b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php @@ -0,0 +1,115 @@ + __( 'Your site serves static assets with far-future expiration headers', 'performance-lab' ), + 'status' => 'good', + 'badge' => array( + 'label' => __( 'Performance', 'performance-lab' ), + 'color' => 'blue', + ), + 'description' => sprintf( + '

%s

', + esc_html__( + 'Serving static assets with far-future expiration headers improves performance by allowing browsers to cache files for a long time, reducing repeated requests.', + 'performance-lab' + ) + ), + 'actions' => '', + 'test' => 'is_far_future_headers_enabled', + ); + + // List of assets to check. + $assets = array( + plugins_url( 'far-future-headers/assets/test.css', __DIR__ ), + ); + + // Check if far-future headers are enabled for all assets. + $far_future_enabled = true; + foreach ( $assets as $asset ) { + if ( ! perflab_far_future_headers_is_enabled( $asset ) ) { + $far_future_enabled = false; + break; + } + } + + if ( ! $far_future_enabled ) { + $result['status'] = 'recommended'; + $result['label'] = __( 'Your site does not serve static assets with recommended far-future expiration headers', 'performance-lab' ); + $result['actions'] = sprintf( + '

%s

', + esc_html__( 'Consider adding or adjusting your server configuration (e.g., .htaccess, nginx config, or a caching plugin) to include far-future Cache-Control or Expires headers for static assets.', 'performance-lab' ) + ); + } + + return $result; +} + +/** + * Checks if far-future expiration headers are enabled. + * + * @since n.e.x.t + * + * @param string $url URL to check. + * @return bool True if far-future headers are enabled, false otherwise. + */ +function perflab_far_future_headers_is_enabled( string $url ): bool { + $threshold = YEAR_IN_SECONDS; + + $response = wp_remote_request( $url, array( 'sslverify' => false ) ); + + if ( is_wp_error( $response ) ) { + return false; + } + + $headers = wp_remote_retrieve_headers( $response ); + + $cache_control = isset( $headers['cache-control'] ) ? $headers['cache-control'] : ''; + $expires = isset( $headers['expires'] ) ? $headers['expires'] : ''; + + // Check Cache-Control header for max-age. + $max_age = 0; + if ( $cache_control ) { + // Cache-Control can have multiple directives; we only care about max-age. + $controls = is_array( $cache_control ) ? $cache_control : array( $cache_control ); + foreach ( $controls as $control ) { + if ( (bool) preg_match( '/max-age\s*=\s*(\d+)/', $control, $matches ) ) { + $max_age = (int) $matches[1]; + break; + } + } + } + + // If max-age meets or exceeds the threshold, we consider it good. + if ( $max_age >= $threshold ) { + return true; + } + + // If max-age is not sufficient, check Expires. + // Expires is a date; we want to ensure it's far in the future. + if ( $expires ) { + $expires_time = strtotime( $expires ); + if ( $expires_time && ( $expires_time - time() ) >= $threshold ) { + return true; + } + } + + return false; +} diff --git a/plugins/performance-lab/includes/site-health/far-future-headers/hooks.php b/plugins/performance-lab/includes/site-health/far-future-headers/hooks.php new file mode 100644 index 0000000000..9ca091a167 --- /dev/null +++ b/plugins/performance-lab/includes/site-health/far-future-headers/hooks.php @@ -0,0 +1,28 @@ +} $tests Site Health Tests. + * @return array{direct: array} Amended tests. + */ +function perflab_ffh_add_test( array $tests ): array { + $tests['direct']['far_future_headers'] = array( + 'label' => __( 'Far-Future Caching Headers', 'performance-lab' ), + 'test' => 'perflab_ffh_assets_test', + ); + return $tests; +} +add_filter( 'site_status_tests', 'perflab_ffh_add_test' ); diff --git a/plugins/performance-lab/includes/site-health/load.php b/plugins/performance-lab/includes/site-health/load.php index e4cd71a596..ac0032cf87 100644 --- a/plugins/performance-lab/includes/site-health/load.php +++ b/plugins/performance-lab/includes/site-health/load.php @@ -29,3 +29,7 @@ // AVIF headers site health check. require_once __DIR__ . '/avif-headers/helper.php'; require_once __DIR__ . '/avif-headers/hooks.php'; + +// Far-Future Headers site health check. +require_once __DIR__ . '/far-future-headers/helper.php'; +require_once __DIR__ . '/far-future-headers/hooks.php'; From 4cb07aac678ad596d1d8bb6cd8c8a50ca7584b2b Mon Sep 17 00:00:00 2001 From: b1ink0 Date: Mon, 9 Dec 2024 18:25:49 +0530 Subject: [PATCH 02/12] Add filter for customizing far-future headers threshold --- .../includes/site-health/far-future-headers/helper.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/performance-lab/includes/site-health/far-future-headers/helper.php b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php index 180c51cb53..5ac2b2963c 100644 --- a/plugins/performance-lab/includes/site-health/far-future-headers/helper.php +++ b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php @@ -71,7 +71,14 @@ function perflab_ffh_assets_test(): array { * @return bool True if far-future headers are enabled, false otherwise. */ function perflab_far_future_headers_is_enabled( string $url ): bool { - $threshold = YEAR_IN_SECONDS; + /** + * Filters the threshold for far-future headers. + * + * @since n.e.x.t + * + * @param int $threshold Threshold in seconds. + */ + $threshold = apply_filters( 'perflab_far_future_headers_threshold', YEAR_IN_SECONDS ); $response = wp_remote_request( $url, array( 'sslverify' => false ) ); From ec9a552b2450f4a7aeabeee7dfb7ab15f6982c1c Mon Sep 17 00:00:00 2001 From: b1ink0 Date: Mon, 9 Dec 2024 19:52:47 +0530 Subject: [PATCH 03/12] Refactor far-future headers checks and improve asset validation logic --- .../site-health/far-future-headers/helper.php | 107 ++++++++++++++---- 1 file changed, 84 insertions(+), 23 deletions(-) diff --git a/plugins/performance-lab/includes/site-health/far-future-headers/helper.php b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php index 5ac2b2963c..0cd8b1092f 100644 --- a/plugins/performance-lab/includes/site-health/far-future-headers/helper.php +++ b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php @@ -42,35 +42,69 @@ function perflab_ffh_assets_test(): array { ); // Check if far-future headers are enabled for all assets. - $far_future_enabled = true; - foreach ( $assets as $asset ) { - if ( ! perflab_far_future_headers_is_enabled( $asset ) ) { - $far_future_enabled = false; - break; - } - } + $status = perflab_ffh_check_assets( $assets ); - if ( ! $far_future_enabled ) { - $result['status'] = 'recommended'; + if ( 'good' !== $status ) { + $result['status'] = $status; $result['label'] = __( 'Your site does not serve static assets with recommended far-future expiration headers', 'performance-lab' ); $result['actions'] = sprintf( '

%s

', - esc_html__( 'Consider adding or adjusting your server configuration (e.g., .htaccess, nginx config, or a caching plugin) to include far-future Cache-Control or Expires headers for static assets.', 'performance-lab' ) + esc_html__( 'Far-future Cache-Control or Expires headers can be added or adjusted with a small configuration change by your hosting provider.', 'performance-lab' ) ); } return $result; } +/** + * Checks if far-future expiration headers are enabled for a list of assets. + * + * @since n.e.x.t + * + * @param string[] $assets List of asset URLs to check. + * @return string 'good' if far-future headers are enabled for all assets, 'recommended' if some assets are missing headers. + */ +function perflab_ffh_check_assets( array $assets ): string { + $final_status = 'good'; + + foreach ( $assets as $asset ) { + $response = wp_remote_get( $asset, array( 'sslverify' => false ) ); + + if ( is_wp_error( $response ) ) { + continue; + } + + $headers = wp_remote_retrieve_headers( $response ); + + if ( is_array( $headers ) ) { + continue; + } + + if ( perflab_ffh_check_headers( $headers ) ) { + continue; + } + + // If far-future headers are not enabled, attempt a conditional request. + if ( perflab_ffh_try_conditional_request( $asset, $headers ) ) { + $final_status = 'recommended'; + continue; + } + + $final_status = 'recommended'; + } + + return $final_status; +} + /** * Checks if far-future expiration headers are enabled. * * @since n.e.x.t * - * @param string $url URL to check. + * @param WpOrg\Requests\Utility\CaseInsensitiveDictionary $headers Response headers. * @return bool True if far-future headers are enabled, false otherwise. */ -function perflab_far_future_headers_is_enabled( string $url ): bool { +function perflab_ffh_check_headers( WpOrg\Requests\Utility\CaseInsensitiveDictionary $headers ): bool { /** * Filters the threshold for far-future headers. * @@ -80,20 +114,12 @@ function perflab_far_future_headers_is_enabled( string $url ): bool { */ $threshold = apply_filters( 'perflab_far_future_headers_threshold', YEAR_IN_SECONDS ); - $response = wp_remote_request( $url, array( 'sslverify' => false ) ); - - if ( is_wp_error( $response ) ) { - return false; - } - - $headers = wp_remote_retrieve_headers( $response ); - $cache_control = isset( $headers['cache-control'] ) ? $headers['cache-control'] : ''; $expires = isset( $headers['expires'] ) ? $headers['expires'] : ''; // Check Cache-Control header for max-age. $max_age = 0; - if ( $cache_control ) { + if ( '' !== $cache_control ) { // Cache-Control can have multiple directives; we only care about max-age. $controls = is_array( $cache_control ) ? $cache_control : array( $cache_control ); foreach ( $controls as $control ) { @@ -111,12 +137,47 @@ function perflab_far_future_headers_is_enabled( string $url ): bool { // If max-age is not sufficient, check Expires. // Expires is a date; we want to ensure it's far in the future. - if ( $expires ) { + if ( is_string( $expires ) && '' !== $expires ) { $expires_time = strtotime( $expires ); - if ( $expires_time && ( $expires_time - time() ) >= $threshold ) { + if ( (bool) $expires_time && ( $expires_time - time() ) >= $threshold ) { return true; } } return false; } + +/** + * Attempt a conditional request with ETag/Last-Modified. + * + * @param string $url The asset URL. + * @param WpOrg\Requests\Utility\CaseInsensitiveDictionary $headers The initial response headers. + * @return bool True if a 304 response was received. + */ +function perflab_ffh_try_conditional_request( string $url, WpOrg\Requests\Utility\CaseInsensitiveDictionary $headers ): bool { + $etag = isset( $headers['etag'] ) ? $headers['etag'] : ''; + $last_modified = isset( $headers['last-modified'] ) ? $headers['last-modified'] : ''; + + $conditional_headers = array(); + if ( '' !== $etag ) { + $conditional_headers['If-None-Match'] = $etag; + } + if ( '' !== $last_modified ) { + $conditional_headers['If-Modified-Since'] = $last_modified; + } + + $response = wp_remote_get( + $url, + array( + 'sslverify' => false, + 'headers' => $conditional_headers, + ) + ); + + if ( is_wp_error( $response ) ) { + return false; + } + + $status_code = wp_remote_retrieve_response_code( $response ); + return ( 304 === $status_code ); +} From 0078cfc151d072d06a2ec0ab3a1d5e9fd9219329 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Tue, 10 Dec 2024 14:05:54 +0530 Subject: [PATCH 04/12] Use assets from wp-includes for far-future headers site health check --- .../includes/site-health/far-future-headers/assets/test.css | 0 .../includes/site-health/far-future-headers/helper.php | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) delete mode 100644 plugins/performance-lab/includes/site-health/far-future-headers/assets/test.css diff --git a/plugins/performance-lab/includes/site-health/far-future-headers/assets/test.css b/plugins/performance-lab/includes/site-health/far-future-headers/assets/test.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/performance-lab/includes/site-health/far-future-headers/helper.php b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php index 0cd8b1092f..c6cc7824ec 100644 --- a/plugins/performance-lab/includes/site-health/far-future-headers/helper.php +++ b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php @@ -38,7 +38,10 @@ function perflab_ffh_assets_test(): array { // List of assets to check. $assets = array( - plugins_url( 'far-future-headers/assets/test.css', __DIR__ ), + includes_url( 'js/wp-embed.min.js' ), + includes_url( 'css/buttons.min.css' ), + includes_url( 'fonts/dashicons.woff2' ), + includes_url( 'images/media/video.png' ), ); // Check if far-future headers are enabled for all assets. From 3e5d958c424315f1863f66a29fa581004d272010 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Tue, 10 Dec 2024 14:08:47 +0530 Subject: [PATCH 05/12] Add filter to customize assets checked for far-future headers --- .../includes/site-health/far-future-headers/helper.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/performance-lab/includes/site-health/far-future-headers/helper.php b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php index c6cc7824ec..8edf4e76e5 100644 --- a/plugins/performance-lab/includes/site-health/far-future-headers/helper.php +++ b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php @@ -44,6 +44,15 @@ function perflab_ffh_assets_test(): array { includes_url( 'images/media/video.png' ), ); + /** + * Filters the list of assets to check for far-future headers. + * + * @since n.e.x.t + * + * @param string[] $assets List of asset URLs to check. + */ + $assets = apply_filters( 'perflab_ffh_assets_to_check', $assets ); + // Check if far-future headers are enabled for all assets. $status = perflab_ffh_check_assets( $assets ); From 35e79775d7b4a0188543b0be2852f459505c06aa Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Tue, 10 Dec 2024 14:58:07 +0530 Subject: [PATCH 06/12] Enhance far-future headers check to return detailed results and generate a report table for missing headers --- .../site-health/far-future-headers/helper.php | 93 ++++++++++++++----- 1 file changed, 69 insertions(+), 24 deletions(-) diff --git a/plugins/performance-lab/includes/site-health/far-future-headers/helper.php b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php index 8edf4e76e5..03c9fbdc41 100644 --- a/plugins/performance-lab/includes/site-health/far-future-headers/helper.php +++ b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php @@ -54,15 +54,25 @@ function perflab_ffh_assets_test(): array { $assets = apply_filters( 'perflab_ffh_assets_to_check', $assets ); // Check if far-future headers are enabled for all assets. - $status = perflab_ffh_check_assets( $assets ); - - if ( 'good' !== $status ) { - $result['status'] = $status; - $result['label'] = __( 'Your site does not serve static assets with recommended far-future expiration headers', 'performance-lab' ); - $result['actions'] = sprintf( - '

%s

', - esc_html__( 'Far-future Cache-Control or Expires headers can be added or adjusted with a small configuration change by your hosting provider.', 'performance-lab' ) - ); + $results = perflab_ffh_check_assets( $assets ); + + if ( 'good' !== $results['final_status'] ) { + $result['status'] = $results['final_status']; + $result['label'] = __( 'Your site does not serve static assets with recommended far-future expiration headers', 'performance-lab' ); + + if ( count( $results['details'] ) > 0 ) { + $table_html = perflab_ffh_get_extensions_table( $results['details'] ); + $result['actions'] = sprintf( + '

%s

%s', + esc_html__( 'The following file types do not have the recommended far-future headers. Consider adding or adjusting Cache-Control or Expires headers for these asset types.', 'performance-lab' ), + $table_html + ); + } else { + $result['actions'] = sprintf( + '

%s

', + esc_html__( 'Far-future Cache-Control or Expires headers can be added or adjusted with a small configuration change by your hosting provider.', 'performance-lab' ) + ); + } } return $result; @@ -74,38 +84,45 @@ function perflab_ffh_assets_test(): array { * @since n.e.x.t * * @param string[] $assets List of asset URLs to check. - * @return string 'good' if far-future headers are enabled for all assets, 'recommended' if some assets are missing headers. + * @return array{final_status: string, details: string[]} Final status and details. */ -function perflab_ffh_check_assets( array $assets ): string { - $final_status = 'good'; +function perflab_ffh_check_assets( array $assets ): array { + $final_status = 'good'; + $extension_results = array(); // Extensions that need improvement. foreach ( $assets as $asset ) { $response = wp_remote_get( $asset, array( 'sslverify' => false ) ); + // Extract extension from the URL. + $path_info = pathinfo( (string) wp_parse_url( $asset, PHP_URL_PATH ) ); + $extension = isset( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : 'unknown'; + if ( is_wp_error( $response ) ) { continue; } $headers = wp_remote_retrieve_headers( $response ); - - if ( is_array( $headers ) ) { + if ( ! is_object( $headers ) ) { continue; } - if ( perflab_ffh_check_headers( $headers ) ) { - continue; - } + if ( ! perflab_ffh_check_headers( $headers ) ) { + if ( ! perflab_ffh_try_conditional_request( $asset, $headers ) ) { + $final_status = 'recommended'; + $extension_results[] = $extension; + continue; + } - // If far-future headers are not enabled, attempt a conditional request. - if ( perflab_ffh_try_conditional_request( $asset, $headers ) ) { - $final_status = 'recommended'; - continue; + // Conditional pass means still recommended, not fully good. + $final_status = 'recommended'; + $extension_results[] = $extension; } - - $final_status = 'recommended'; } - return $final_status; + return array( + 'final_status' => $final_status, + 'details' => $extension_results, + ); } /** @@ -193,3 +210,31 @@ function perflab_ffh_try_conditional_request( string $url, WpOrg\Requests\Utilit $status_code = wp_remote_retrieve_response_code( $response ); return ( 304 === $status_code ); } + +/** + * Generate a table listing file extensions that need far-future headers. + * + * @since n.e.x.t + * + * @param string[] $extensions Array of file extensions needing improvement. + * @return string HTML formatted table. + */ +function perflab_ffh_get_extensions_table( array $extensions ): string { + $html_table = sprintf( + '', + esc_html__( 'File Extension', 'performance-lab' ), + esc_html__( 'Status', 'performance-lab' ) + ); + + foreach ( $extensions as $extension ) { + $html_table .= sprintf( + '', + esc_html( $extension ), + esc_html__( 'Needs far-future headers', 'performance-lab' ) + ); + } + + $html_table .= '
%s%s
%s%s
'; + + return $html_table; +} From 3db58860f1bbbd47a23824e05adb2aa8938d6125 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Tue, 10 Dec 2024 16:03:34 +0530 Subject: [PATCH 07/12] Improve far-future headers checks by adding detailed failure reasons and updating the extensions table generation --- .../site-health/far-future-headers/helper.php | 117 +++++++++++++----- 1 file changed, 88 insertions(+), 29 deletions(-) diff --git a/plugins/performance-lab/includes/site-health/far-future-headers/helper.php b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php index 03c9fbdc41..ae226b608e 100644 --- a/plugins/performance-lab/includes/site-health/far-future-headers/helper.php +++ b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php @@ -61,11 +61,11 @@ function perflab_ffh_assets_test(): array { $result['label'] = __( 'Your site does not serve static assets with recommended far-future expiration headers', 'performance-lab' ); if ( count( $results['details'] ) > 0 ) { - $table_html = perflab_ffh_get_extensions_table( $results['details'] ); $result['actions'] = sprintf( - '

%s

%s', + '

%s

%s

%s

', esc_html__( 'The following file types do not have the recommended far-future headers. Consider adding or adjusting Cache-Control or Expires headers for these asset types.', 'performance-lab' ), - $table_html + perflab_ffh_get_extensions_table( $results['details'] ), + esc_html__( 'Note: "Conditionally cached" means that the browser can re-validate the resource using ETag or Last-Modified headers. This results in fewer full downloads but still requires the browser to make requests, unlike far-future expiration headers that allow the browser to fully rely on its local cache for a longer duration.', 'performance-lab' ) ); } else { $result['actions'] = sprintf( @@ -84,11 +84,11 @@ function perflab_ffh_assets_test(): array { * @since n.e.x.t * * @param string[] $assets List of asset URLs to check. - * @return array{final_status: string, details: string[]} Final status and details. + * @return array{final_status: string, details: array{extension: string, reason: string}[]} Final status and details. */ function perflab_ffh_check_assets( array $assets ): array { - $final_status = 'good'; - $extension_results = array(); // Extensions that need improvement. + $final_status = 'good'; + $fail_details = array(); // Array of arrays with 'extension' and 'reason'. foreach ( $assets as $asset ) { $response = wp_remote_get( $asset, array( 'sslverify' => false ) ); @@ -98,30 +98,62 @@ function perflab_ffh_check_assets( array $assets ): array { $extension = isset( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : 'unknown'; if ( is_wp_error( $response ) ) { + // Can't determine headers if request failed, consider it a fail. + $final_status = 'recommended'; + $fail_details[] = array( + 'extension' => $extension, + 'reason' => __( 'Could not retrieve headers', 'performance-lab' ), + ); continue; } $headers = wp_remote_retrieve_headers( $response ); if ( ! is_object( $headers ) ) { + // No valid headers retrieved. + $final_status = 'recommended'; + $fail_details[] = array( + 'extension' => $extension, + 'reason' => __( 'No valid headers retrieved', 'performance-lab' ), + ); continue; } - if ( ! perflab_ffh_check_headers( $headers ) ) { - if ( ! perflab_ffh_try_conditional_request( $asset, $headers ) ) { - $final_status = 'recommended'; - $extension_results[] = $extension; - continue; - } + $check = perflab_ffh_check_headers( $headers ); + if ( isset( $check['passed'] ) && $check['passed'] ) { + // This asset passed far-future headers test, no action needed. + continue; + } - // Conditional pass means still recommended, not fully good. - $final_status = 'recommended'; - $extension_results[] = $extension; + // If not passed, decide whether to try conditional request. + if ( false === $check ) { + // Only if no far-future headers at all, we try conditional request. + $conditional_pass = perflab_ffh_try_conditional_request( $asset, $headers ); + if ( ! $conditional_pass ) { + $final_status = 'recommended'; + $fail_details[] = array( + 'extension' => $extension, + 'reason' => __( 'No far-future headers and no conditional caching', 'performance-lab' ), + ); + } else { + $final_status = 'recommended'; + $fail_details[] = array( + 'extension' => $extension, + 'reason' => __( 'No far-future headers but conditionally cached', 'performance-lab' ), + ); + } + } else { + // If there's a max-age or expires but below threshold, we skip conditional. + $final_status = 'recommended'; + $fail_details[] = array( + 'extension' => $extension, + 'reason' => $check['reason'], + ); } } return array( 'final_status' => $final_status, - 'details' => $extension_results, + 'details' => $fail_details, ); } @@ -131,9 +163,9 @@ function perflab_ffh_check_assets( array $assets ): array { * @since n.e.x.t * * @param WpOrg\Requests\Utility\CaseInsensitiveDictionary $headers Response headers. - * @return bool True if far-future headers are enabled, false otherwise. + * @return array{passed: bool, reason: string}|false Detailed result. If passed=false, reason explains why it failed and false if no headers found. */ -function perflab_ffh_check_headers( WpOrg\Requests\Utility\CaseInsensitiveDictionary $headers ): bool { +function perflab_ffh_check_headers( WpOrg\Requests\Utility\CaseInsensitiveDictionary $headers ) { /** * Filters the threshold for far-future headers. * @@ -161,19 +193,46 @@ function perflab_ffh_check_headers( WpOrg\Requests\Utility\CaseInsensitiveDictio // If max-age meets or exceeds the threshold, we consider it good. if ( $max_age >= $threshold ) { - return true; + return array( + 'passed' => true, + 'reason' => '', + ); } - // If max-age is not sufficient, check Expires. - // Expires is a date; we want to ensure it's far in the future. + // If max-age is too low or not present, check Expires. if ( is_string( $expires ) && '' !== $expires ) { $expires_time = strtotime( $expires ); if ( (bool) $expires_time && ( $expires_time - time() ) >= $threshold ) { - return true; + // Good - Expires far in the future. + return array( + 'passed' => true, + 'reason' => '', + ); + } + + // Expires header exists but not far enough in the future. + if ( $max_age > 0 && $max_age < $threshold ) { + return array( + 'passed' => false, + 'reason' => __( 'max-age below threshold', 'performance-lab' ), + ); } + return array( + 'passed' => false, + 'reason' => __( 'expires below threshold', 'performance-lab' ), + ); } - return false; + // No max-age or expires found at all or max-age < threshold and no expires. + if ( 0 === $max_age ) { + return false; + } else { + // max-age was present but below threshold and no expires. + return array( + 'passed' => false, + 'reason' => __( 'max-age below threshold', 'performance-lab' ), + ); + } } /** @@ -212,25 +271,25 @@ function perflab_ffh_try_conditional_request( string $url, WpOrg\Requests\Utilit } /** - * Generate a table listing file extensions that need far-future headers. + * Generate a table listing file extensions that need far-future headers, including reasons. * * @since n.e.x.t * - * @param string[] $extensions Array of file extensions needing improvement. + * @param array $fail_details Array of arrays with 'extension' and 'reason'. * @return string HTML formatted table. */ -function perflab_ffh_get_extensions_table( array $extensions ): string { +function perflab_ffh_get_extensions_table( array $fail_details ): string { $html_table = sprintf( '', esc_html__( 'File Extension', 'performance-lab' ), esc_html__( 'Status', 'performance-lab' ) ); - foreach ( $extensions as $extension ) { + foreach ( $fail_details as $detail ) { $html_table .= sprintf( '', - esc_html( $extension ), - esc_html__( 'Needs far-future headers', 'performance-lab' ) + esc_html( $detail['extension'] ), + esc_html( $detail['reason'] ) ); } From 655d4e58e2e647caa105d24b5e931a4ded172cb0 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Wed, 11 Dec 2024 23:56:08 +0530 Subject: [PATCH 08/12] Add test to ensure status is 'good' when all headers are valid --- .../test-far-future-headers.php | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php diff --git a/plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php b/plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php new file mode 100644 index 0000000000..c49466ad3f --- /dev/null +++ b/plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php @@ -0,0 +1,81 @@ +> + */ + protected $mocked_responses = array(); + + /** + * Setup each test. + */ + public function setUp(): void { + parent::setUp(); + + // Clear any filters or mocks. + remove_all_filters( 'pre_http_request' ); + + // Add the filter to mock HTTP requests. + add_filter( 'pre_http_request', array( $this, 'mock_http_requests' ), 10, 3 ); + } + + /** + * Test that when all assets have valid far-future headers, the status is "good". + */ + public function test_all_assets_valid_far_future_headers(): void { + // Mock responses: all assets have a max-age > 1 year (threshold). + $this->mocked_responses = array( + includes_url( 'js/wp-embed.min.js' ) => $this->build_response( 200, array( 'cache-control' => 'max-age=' . ( YEAR_IN_SECONDS + 1000 ) ) ), + includes_url( 'css/buttons.min.css' ) => $this->build_response( 200, array( 'cache-control' => 'max-age=' . ( YEAR_IN_SECONDS + 500 ) ) ), + includes_url( 'fonts/dashicons.woff2' ) => $this->build_response( 200, array( 'expires' => gmdate( 'D, d M Y H:i:s', time() + YEAR_IN_SECONDS + 1000 ) . ' GMT' ) ), + includes_url( 'images/media/video.png' ) => $this->build_response( 200, array( 'cache-control' => 'max-age=' . ( YEAR_IN_SECONDS + 2000 ) ) ), + ); + + $result = perflab_ffh_assets_test(); + $this->assertEquals( 'good', $result['status'] ); + $this->assertEmpty( $result['actions'] ); + } + + /** + * Mock HTTP requests for assets to simulate different responses. + * + * @param bool $response A preemptive return value of an HTTP request. Default false. + * @param array $args Request arguments. + * @param string $url The request URL. + * @return array Mocked response. + */ + public function mock_http_requests( bool $response, array $args, string $url ): array { + if ( isset( $this->mocked_responses[ $url ] ) ) { + return $this->mocked_responses[ $url ]; + } + + // If no specific mock set, default to a generic success with no caching. + return $this->build_response( 200 ); + } + + /** + * Helper method to build a mock HTTP response. + * + * @param int $status_code HTTP status code. + * @param array $headers HTTP headers. + * @return array{response: array{code: int, message: string}, headers: WpOrg\Requests\Utility\CaseInsensitiveDictionary} + */ + protected function build_response( int $status_code = 200, array $headers = array() ): array { + return array( + 'response' => array( + 'code' => $status_code, + 'message' => '', + ), + 'headers' => new WpOrg\Requests\Utility\CaseInsensitiveDictionary( $headers ), + ); + } +} From 20099bf04e6972833989108065742a83286a62c0 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Thu, 12 Dec 2024 13:13:57 +0530 Subject: [PATCH 09/12] Add test for assets with conditional caching --- .../test-far-future-headers.php | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php b/plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php index c49466ad3f..c33025d589 100644 --- a/plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php +++ b/plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php @@ -45,6 +45,36 @@ public function test_all_assets_valid_far_future_headers(): void { $this->assertEmpty( $result['actions'] ); } + /** + * Test that when an asset has no far-future headers but has conditional caching (ETag/Last-Modified), status is 'recommended'. + */ + public function test_assets_conditionally_cached(): void { + // For conditional caching scenario, setting etag/last-modified headers. + $this->mocked_responses = array( + 'js/wp-embed.min.js' => $this->build_response( 200, array( 'cache-control' => 'max-age=' . ( YEAR_IN_SECONDS + 1000 ) ) ), + 'css/buttons.min.css' => $this->build_response( 200, array( 'etag' => '"123456789"' ) ), + 'fonts/dashicons.woff2' => $this->build_response( 200, array( 'last-modified' => gmdate( 'D, d M Y H:i:s', time() - 1000 ) . ' GMT' ) ), + 'images/media/video.png' => $this->build_response( + 200, + array( + 'etag' => '"123456789"', + 'last-modified' => gmdate( 'D, d M Y H:i:s', time() - 1000 ) . ' GMT', + ) + ), + ); + + // For the asset with just ETag/Last-Modified and no far-future headers, perflab_ffh_try_conditional_request will be attempted. + $this->mocked_responses['conditional_304'] = array( + 'response' => array( 'code' => 304 ), + 'headers' => array(), + 'body' => '', + ); + + $result = perflab_ffh_assets_test(); + $this->assertEquals( 'recommended', $result['status'] ); + $this->assertNotEmpty( $result['actions'] ); + } + /** * Mock HTTP requests for assets to simulate different responses. * @@ -54,6 +84,11 @@ public function test_all_assets_valid_far_future_headers(): void { * @return array Mocked response. */ public function mock_http_requests( bool $response, array $args, string $url ): array { + // If conditional headers used in second request, simulate a 304 response. + if ( isset( $this->mocked_responses['conditional_304'] ) && ( isset( $args['headers']['If-None-Match'] ) || isset( $args['headers']['If-Modified-Since'] ) ) ) { + return $this->mocked_responses['conditional_304']; + } + if ( isset( $this->mocked_responses[ $url ] ) ) { return $this->mocked_responses[ $url ]; } From 54f36214aa80edfea21c4f0800788a5f12d374bf Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Thu, 12 Dec 2024 16:58:22 +0530 Subject: [PATCH 10/12] Add test for status messages based on far-future headers results --- .../test-far-future-headers.php | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php b/plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php index c33025d589..cd89e36a58 100644 --- a/plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php +++ b/plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php @@ -61,13 +61,7 @@ public function test_assets_conditionally_cached(): void { 'last-modified' => gmdate( 'D, d M Y H:i:s', time() - 1000 ) . ' GMT', ) ), - ); - - // For the asset with just ETag/Last-Modified and no far-future headers, perflab_ffh_try_conditional_request will be attempted. - $this->mocked_responses['conditional_304'] = array( - 'response' => array( 'code' => 304 ), - 'headers' => array(), - 'body' => '', + 'conditional_304' => $this->build_response( 304 ), ); $result = perflab_ffh_assets_test(); @@ -75,6 +69,34 @@ public function test_assets_conditionally_cached(): void { $this->assertNotEmpty( $result['actions'] ); } + /** + * Test that different status messages are returned based on the test results. + */ + public function test_status_messages(): void { + $this->mocked_responses = array( + includes_url( 'js/wp-embed.min.js' ) => $this->build_response( 200, array( 'cache-control' => 'max-age=' . ( YEAR_IN_SECONDS - 1000 ) ) ), + includes_url( 'css/buttons.min.css' ) => $this->build_response( 200, array( 'expires' => gmdate( 'D, d M Y H:i:s', time() + YEAR_IN_SECONDS - 1000 ) . ' GMT' ) ), + includes_url( 'fonts/dashicons.woff2' ) => $this->build_response( 200, array( 'etag' => '"123456789"' ) ), + includes_url( 'images/media/video.png' ) => $this->build_response( 200, array() ), + 'conditional_304' => $this->build_response( 304 ), + ); + + $result = perflab_ffh_check_assets( + array( + includes_url( 'js/wp-embed.min.js' ), + includes_url( 'css/buttons.min.css' ), + includes_url( 'fonts/dashicons.woff2' ), + includes_url( 'images/media/video.png' ), + ) + ); + + $this->assertEquals( 'recommended', $result['final_status'] ); + $this->assertEquals( 'max-age below threshold', $result['details'][0]['reason'] ); + $this->assertEquals( 'expires below threshold', $result['details'][1]['reason'] ); + $this->assertEquals( 'No far-future headers but conditionally cached', $result['details'][2]['reason'] ); + $this->assertEquals( 'No far-future headers and no conditional caching', $result['details'][3]['reason'] ); + } + /** * Mock HTTP requests for assets to simulate different responses. * From 0a776c57e30adb76b46e53bd5c95454ccc4226e0 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Thu, 12 Dec 2024 17:15:34 +0530 Subject: [PATCH 11/12] Add tests for filters affecting far-future headers asset checks --- .../test-far-future-headers.php | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php b/plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php index cd89e36a58..89484158df 100644 --- a/plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php +++ b/plugins/performance-lab/tests/includes/site-health/far-future-headers/test-far-future-headers.php @@ -97,6 +97,49 @@ public function test_status_messages(): void { $this->assertEquals( 'No far-future headers and no conditional caching', $result['details'][3]['reason'] ); } + /** + * Test that the filter `perflab_ffh_assets_to_check` and `perflab_far_future_headers_threshold` are working as expected. + */ + public function test_filters(): void { + add_filter( + 'perflab_ffh_assets_to_check', + static function ( $assets ) { + $assets[] = includes_url( 'images/blank.gif' ); + return $assets; + } + ); + + add_filter( + 'perflab_far_future_headers_threshold', + static function () { + return 1000; + } + ); + + $this->mocked_responses = array( + includes_url( 'js/wp-embed.min.js' ) => $this->build_response( 200, array( 'cache-control' => 'max-age=' . 1500 ) ), + includes_url( 'css/buttons.min.css' ) => $this->build_response( 200, array( 'cache-control' => 'max-age=' . 500 ) ), + includes_url( 'fonts/dashicons.woff2' ) => $this->build_response( 200, array( 'expires' => gmdate( 'D, d M Y H:i:s', time() + 1500 ) . ' GMT' ) ), + includes_url( 'images/media/video.png' ) => $this->build_response( 200, array( 'expires' => gmdate( 'D, d M Y H:i:s', time() + 500 ) . ' GMT' ) ), + includes_url( 'images/blank.gif' ) => $this->build_response( 200, array( 'cache-control' => 'max-age=' . ( 500 ) ) ), + ); + + $result = perflab_ffh_check_assets( + array( + includes_url( 'js/wp-embed.min.js' ), + includes_url( 'css/buttons.min.css' ), + includes_url( 'fonts/dashicons.woff2' ), + includes_url( 'images/media/video.png' ), + includes_url( 'images/blank.gif' ), + ) + ); + + $this->assertEquals( 'recommended', $result['final_status'] ); + $this->assertEquals( 'max-age below threshold', $result['details'][0]['reason'] ); + $this->assertEquals( 'expires below threshold', $result['details'][1]['reason'] ); + $this->assertEquals( 'max-age below threshold', $result['details'][2]['reason'] ); + } + /** * Mock HTTP requests for assets to simulate different responses. * From 3c9833739bb096d3f870f147d3a4eac299eda153 Mon Sep 17 00:00:00 2001 From: Aditya Dhade Date: Thu, 12 Dec 2024 17:33:39 +0530 Subject: [PATCH 12/12] Refactor to use filenames instead of extenstions in failure details --- .../site-health/far-future-headers/helper.php | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/plugins/performance-lab/includes/site-health/far-future-headers/helper.php b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php index ae226b608e..f3077de41b 100644 --- a/plugins/performance-lab/includes/site-health/far-future-headers/helper.php +++ b/plugins/performance-lab/includes/site-health/far-future-headers/helper.php @@ -84,25 +84,25 @@ function perflab_ffh_assets_test(): array { * @since n.e.x.t * * @param string[] $assets List of asset URLs to check. - * @return array{final_status: string, details: array{extension: string, reason: string}[]} Final status and details. + * @return array{final_status: string, details: array{filename: string, reason: string}[]} Final status and details. */ function perflab_ffh_check_assets( array $assets ): array { $final_status = 'good'; - $fail_details = array(); // Array of arrays with 'extension' and 'reason'. + $fail_details = array(); // Array of arrays with 'filename' and 'reason'. foreach ( $assets as $asset ) { $response = wp_remote_get( $asset, array( 'sslverify' => false ) ); - // Extract extension from the URL. + // Extract filename from the URL. $path_info = pathinfo( (string) wp_parse_url( $asset, PHP_URL_PATH ) ); - $extension = isset( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : 'unknown'; + $filename = isset( $path_info['basename'] ) ? $path_info['basename'] : basename( $asset ); if ( is_wp_error( $response ) ) { // Can't determine headers if request failed, consider it a fail. $final_status = 'recommended'; $fail_details[] = array( - 'extension' => $extension, - 'reason' => __( 'Could not retrieve headers', 'performance-lab' ), + 'filename' => $filename, + 'reason' => __( 'Could not retrieve headers', 'performance-lab' ), ); continue; } @@ -112,8 +112,8 @@ function perflab_ffh_check_assets( array $assets ): array { // No valid headers retrieved. $final_status = 'recommended'; $fail_details[] = array( - 'extension' => $extension, - 'reason' => __( 'No valid headers retrieved', 'performance-lab' ), + 'filename' => $filename, + 'reason' => __( 'No valid headers retrieved', 'performance-lab' ), ); continue; } @@ -131,22 +131,22 @@ function perflab_ffh_check_assets( array $assets ): array { if ( ! $conditional_pass ) { $final_status = 'recommended'; $fail_details[] = array( - 'extension' => $extension, - 'reason' => __( 'No far-future headers and no conditional caching', 'performance-lab' ), + 'filename' => $filename, + 'reason' => __( 'No far-future headers and no conditional caching', 'performance-lab' ), ); } else { $final_status = 'recommended'; $fail_details[] = array( - 'extension' => $extension, - 'reason' => __( 'No far-future headers but conditionally cached', 'performance-lab' ), + 'filename' => $filename, + 'reason' => __( 'No far-future headers but conditionally cached', 'performance-lab' ), ); } } else { // If there's a max-age or expires but below threshold, we skip conditional. $final_status = 'recommended'; $fail_details[] = array( - 'extension' => $extension, - 'reason' => $check['reason'], + 'filename' => $filename, + 'reason' => $check['reason'], ); } } @@ -181,7 +181,7 @@ function perflab_ffh_check_headers( WpOrg\Requests\Utility\CaseInsensitiveDictio // Check Cache-Control header for max-age. $max_age = 0; if ( '' !== $cache_control ) { - // Cache-Control can have multiple directives; we only care about max-age. + // There can be multiple cache-control headers, we only care about max-age. $controls = is_array( $cache_control ) ? $cache_control : array( $cache_control ); foreach ( $controls as $control ) { if ( (bool) preg_match( '/max-age\s*=\s*(\d+)/', $control, $matches ) ) { @@ -271,24 +271,24 @@ function perflab_ffh_try_conditional_request( string $url, WpOrg\Requests\Utilit } /** - * Generate a table listing file extensions that need far-future headers, including reasons. + * Generate a table listing files that need far-future headers, including reasons. * * @since n.e.x.t * - * @param array $fail_details Array of arrays with 'extension' and 'reason'. + * @param array $fail_details Array of arrays with 'filename' and 'reason'. * @return string HTML formatted table. */ function perflab_ffh_get_extensions_table( array $fail_details ): string { $html_table = sprintf( '
%s%s
%s%s
', - esc_html__( 'File Extension', 'performance-lab' ), + esc_html__( 'File', 'performance-lab' ), esc_html__( 'Status', 'performance-lab' ) ); foreach ( $fail_details as $detail ) { $html_table .= sprintf( '', - esc_html( $detail['extension'] ), + esc_html( $detail['filename'] ), esc_html( $detail['reason'] ) ); }
%s%s
%s%s