<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[54043] trunk: Site Health: Introduce page cache check.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/54043">54043</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/54043","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>flixos90</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2022-08-31 22:44:04 +0000 (Wed, 31 Aug 2022)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Site Health: Introduce page cache check.

This changeset adds a new `page_cache` check which determines whether the site uses a full page cache, and in addition assesses the server response time. If no page cache is present and the server response time is slow, the check will suggest use of a page cache.

A few filters are included for customization of the check:
* `site_status_good_response_time_threshold` filters the number of milliseconds below which the server response time is considered good. The default value is based on the `server-response-time` Lighthouse audit and can be altered using this filter.
* `site_status_page_cache_supported_cache_headers` filters the map of supported cache headers and their callback to determine whether it was a cache hit. The default list includes commonly used cache headers, and it is filterable to support e.g. additional cache headers used by specific vendors.

Note that due to the nature of this check it is only run in production environments.

Props furi3r, westonruter, spacedmonkey, swissspidy, Clorith.
Fixes <a href="https://core.trac.wordpress.org/ticket/56041">#56041</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadminincludesclasswpsitehealthphp">trunk/src/wp-admin/includes/class-wp-site-health.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestsitehealthcontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-site-health-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestschemasetupphp">trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestsitehealthcontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-site-health-controller.php</a></li>
<li><a href="#trunktestsphpunittestssitehealthphp">trunk/tests/phpunit/tests/site-health.php</a></li>
<li><a href="#trunktestsqunitfixtureswpapigeneratedjs">trunk/tests/qunit/fixtures/wp-api-generated.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadminincludesclasswpsitehealthphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/class-wp-site-health.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/class-wp-site-health.php      2022-08-31 22:17:51 UTC (rev 54042)
+++ trunk/src/wp-admin/includes/class-wp-site-health.php        2022-08-31 22:44:04 UTC (rev 54043)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1667,11 +1667,125 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Tests if a full page cache is available.
+        *
+        * @since 6.1.0
+        *
+        * @return array The test result.
+        */
+       public function get_test_page_cache() {
+               $description  = '<p>' . __( 'Page cache enhances the speed and performance of your site by saving and serving static pages instead of calling for a page every time a user visits.' ) . '</p>';
+               $description .= '<p>' . __( 'Page cache is detected by looking for an active page cache plugin as well as making three requests to the homepage and looking for one or more of the following HTTP client caching response headers:' ) . '</p>';
+               $description .= '<code>' . implode( '</code>, <code>', array_keys( $this->get_page_cache_headers() ) ) . '.</code>';
+
+               $result = array(
+                       'badge'       => array(
+                               'label' => __( 'Performance' ),
+                               'color' => 'blue',
+                       ),
+                       'description' => wp_kses_post( $description ),
+                       'test'        => 'page_cache',
+                       'status'      => 'good',
+                       'label'       => '',
+                       'actions'     => sprintf(
+                               '<p><a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s <span class="screen-reader-text">%3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
+                               __( 'https://wordpress.org/support/article/optimization/#Caching' ),
+                               __( 'Learn more about page cache' ),
+                               /* translators: Accessibility text. */
+                               __( '(opens in a new tab)' )
+                       ),
+               );
+
+               $page_cache_detail = $this->get_page_cache_detail();
+
+               if ( is_wp_error( $page_cache_detail ) ) {
+                       $result['label']  = __( 'Unable to detect the presence of page cache' );
+                       $result['status'] = 'recommended';
+                       $error_info       = sprintf(
+                       /* translators: 1 is error message, 2 is error code */
+                               __( 'Unable to detect page cache due to possible loopback request problem. Please verify that the loopback request test is passing. Error: %1$s (Code: %2$s)' ),
+                               $page_cache_detail->get_error_message(),
+                               $page_cache_detail->get_error_code()
+                       );
+                       $result['description'] = wp_kses_post( "<p>$error_info</p>" ) . $result['description'];
+                       return $result;
+               }
+
+               $result['status'] = $page_cache_detail['status'];
+
+               switch ( $page_cache_detail['status'] ) {
+                       case 'recommended':
+                               $result['label'] = __( 'Page cache is not detected but the server response time is OK' );
+                               break;
+                       case 'good':
+                               $result['label'] = __( 'Page cache is detected and the server response time is good' );
+                               break;
+                       default:
+                               if ( empty( $page_cache_detail['headers'] ) && ! $page_cache_detail['advanced_cache_present'] ) {
+                                       $result['label'] = __( 'Page cache is not detected and the server response time is slow' );
+                               } else {
+                                       $result['label'] = __( 'Page cache is detected but the server response time is still slow' );
+                               }
+               }
+
+               $page_cache_test_summary = array();
+
+               if ( empty( $page_cache_detail['response_time'] ) ) {
+                       $page_cache_test_summary[] = '<span class="dashicons dashicons-dismiss"></span> ' . __( 'Server response time could not be determined. Verify that loopback requests are working.' );
+               } else {
+
+                       $threshold = $this->get_good_response_time_threshold();
+                       if ( $page_cache_detail['response_time'] < $threshold ) {
+                               $page_cache_test_summary[] = '<span class="dashicons dashicons-yes-alt"></span> ' . sprintf(
+                                       /* translators: 1: The response time in milliseconds. 2: The recommended threshold milliseconds. */
+                                       __( 'Median server response time was %1$s milliseconds. This is less than the recommended %2$s milliseconds threshold.' ),
+                                       number_format_i18n( $page_cache_detail['response_time'] ),
+                                       number_format_i18n( $threshold )
+                               );
+                       } else {
+                               $page_cache_test_summary[] = '<span class="dashicons dashicons-warning"></span> ' . sprintf(
+                                       /* translators: 1: The response time in milliseconds. 2: The recommended threshold milliseconds. */
+                                       __( 'Median server response time was %1$s milliseconds. It should be less than the recommended %2$s milliseconds threshold.' ),
+                                       number_format_i18n( $page_cache_detail['response_time'] ),
+                                       number_format_i18n( $threshold )
+                               );
+                       }
+
+                       if ( empty( $page_cache_detail['headers'] ) ) {
+                               $page_cache_test_summary[] = '<span class="dashicons dashicons-warning"></span> ' . __( 'No client caching response headers were detected.' );
+                       } else {
+                               $headers_summary  = '<span class="dashicons dashicons-yes-alt"></span>';
+                               $headers_summary .= sprintf(
+                               /* translators: Placeholder is number of caching headers */
+                                       _n(
+                                               ' There was %d client caching response header detected: ',
+                                               ' There were %d client caching response headers detected: ',
+                                               count( $page_cache_detail['headers'] )
+                                       ),
+                                       count( $page_cache_detail['headers'] )
+                               );
+                               $headers_summary          .= '<code>' . implode( '</code>, <code>', $page_cache_detail['headers'] ) . '</code>.';
+                               $page_cache_test_summary[] = $headers_summary;
+                       }
+               }
+
+               if ( $page_cache_detail['advanced_cache_present'] ) {
+                       $page_cache_test_summary[] = '<span class="dashicons dashicons-yes-alt"></span> ' . __( 'A page cache plugin was detected.' );
+               } elseif ( ! ( is_array( $page_cache_detail ) && ! empty( $page_cache_detail['headers'] ) ) ) {
+                       // Note: This message is not shown if client caching response headers were present since an external caching layer may be employed.
+                       $page_cache_test_summary[] = '<span class="dashicons dashicons-warning"></span> ' . __( 'A page cache plugin was not detected.' );
+               }
+
+               $result['description'] .= '<ul><li>' . implode( '</li><li>', $page_cache_test_summary ) . '</li></ul>';
+               return $result;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Check if the HTTP API can handle SSL/TLS requests.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.2.0
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @return array The test results.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @return array The test result.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        public function get_test_ssl_support() {
</span><span class="cx" style="display: block; padding: 0 10px">                $result = array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2482,8 +2596,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Only check for a persistent object cache in production environments to not unnecessarily promote complicated setups.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Only check for caches in production environments.
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( 'production' === wp_get_environment_type() ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        $tests['async']['page_cache'] = array(
+                               'label'             => __( 'Page cache' ),
+                               'test'              => rest_url( 'wp-site-health/v1/tests/page-cache' ),
+                               'has_rest'          => true,
+                               'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_page_cache' ),
+                       );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         $tests['direct']['persistent_object_cache'] = array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'label' => __( 'Persistent object cache' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                'test'  => 'persistent_object_cache',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2966,6 +3087,200 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * 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.
+        *
+        * @since 6.1.0
+        *
+        * @return array List of client caching headers and their (optional) verification callbacks.
+        */
+       public function get_page_cache_headers() {
+
+               $cache_hit_callback = static function ( $header_value ) {
+                       return false !== strpos( strtolower( $header_value ), 'hit' );
+               };
+
+               $cache_headers = array(
+                       'cache-control'          => static function ( $header_value ) {
+                               return (bool) preg_match( '/max-age=[1-9]/', $header_value );
+                       },
+                       'expires'                => static function ( $header_value ) {
+                               return strtotime( $header_value ) > time();
+                       },
+                       'age'                    => static function ( $header_value ) {
+                               return is_numeric( $header_value ) && $header_value > 0;
+                       },
+                       'last-modified'          => '',
+                       'etag'                   => '',
+                       'x-cache-enabled'        => static function ( $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,
+               );
+
+               /**
+                * Filters the list of cache headers supported by core.
+                *
+                * @since 6.1.0
+                *
+                * @param int $cache_headers Array of supported cache headers.
+                */
+               return apply_filters( 'site_status_page_cache_supported_cache_headers', $cache_headers );
+       }
+
+       /**
+        * Checks if site has page cache enabled or not.
+        *
+        * @since 6.1.0
+        *
+        * @return WP_Error|array {
+        *     Page cache detection details or else error information.
+        *
+        *     @type bool    $advanced_cache_present        Whether a page cache plugin is present.
+        *     @type array[] $page_caching_response_headers Sets of client caching headers for the responses.
+        *     @type float[] $response_timing               Response timings.
+        * }
+        */
+       private function check_for_page_caching() {
+
+               /** This filter is documented in wp-includes/class-wp-http-streams.php */
+               $sslverify = apply_filters( 'https_local_ssl_verify', false );
+
+               $headers = array();
+
+               // Include basic auth in loopback requests. Note that this will only pass along basic auth when user is
+               // initiating the test. If a site requires basic auth, the test will fail when it runs in WP Cron as part of
+               // wp_site_health_scheduled_check. This logic is copied from WP_Site_Health::can_perform_loopback().
+               if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
+                       $headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
+               }
+
+               $caching_headers               = $this->get_page_cache_headers();
+               $page_caching_response_headers = array();
+               $response_timing               = array();
+
+               for ( $i = 1; $i <= 3; $i++ ) {
+                       $start_time    = microtime( true );
+                       $http_response = wp_remote_get( home_url( '/' ), compact( 'sslverify', 'headers' ) );
+                       $end_time      = microtime( true );
+
+                       if ( is_wp_error( $http_response ) ) {
+                               return $http_response;
+                       }
+                       if ( wp_remote_retrieve_response_code( $http_response ) !== 200 ) {
+                               return new WP_Error(
+                                       'http_' . wp_remote_retrieve_response_code( $http_response ),
+                                       wp_remote_retrieve_response_message( $http_response )
+                               );
+                       }
+
+                       $response_headers = array();
+
+                       foreach ( $caching_headers as $header => $callback ) {
+                               $header_values = wp_remote_retrieve_header( $http_response, $header );
+                               if ( empty( $header_values ) ) {
+                                       continue;
+                               }
+                               $header_values = (array) $header_values;
+                               if ( empty( $callback ) || ( is_callable( $callback ) && count( array_filter( $header_values, $callback ) ) > 0 ) ) {
+                                       $response_headers[ $header ] = $header_values;
+                               }
+                       }
+
+                       $page_caching_response_headers[] = $response_headers;
+                       $response_timing[]               = ( $end_time - $start_time ) * 1000;
+               }
+
+               return array(
+                       'advanced_cache_present'        => (
+                               file_exists( WP_CONTENT_DIR . '/advanced-cache.php' )
+                               &&
+                               ( defined( 'WP_CACHE' ) && WP_CACHE )
+                               &&
+                               /** This filter is documented in wp-settings.php */
+                               apply_filters( 'enable_loading_advanced_cache_dropin', true )
+                       ),
+                       'page_caching_response_headers' => $page_caching_response_headers,
+                       'response_timing'               => $response_timing,
+               );
+       }
+
+       /**
+        * Get page cache details.
+        *
+        * @since 6.1.0
+        *
+        * @return WP_Error|array {
+        *    Page cache detail or else a WP_Error if unable to determine.
+        *
+        *    @type string   $status                 Page cache status. Good, Recommended or Critical.
+        *    @type bool     $advanced_cache_present Whether page cache plugin is available or not.
+        *    @type string[] $headers                Client caching response headers detected.
+        *    @type float    $response_time          Response time of site.
+        * }
+        */
+       private function get_page_cache_detail() {
+               $page_cache_detail = $this->check_for_page_caching();
+               if ( is_wp_error( $page_cache_detail ) ) {
+                       return $page_cache_detail;
+               }
+
+               // Use the median server response time.
+               $response_timings = $page_cache_detail['response_timing'];
+               rsort( $response_timings );
+               $page_speed = $response_timings[ floor( count( $response_timings ) / 2 ) ];
+
+               // Obtain unique set of all client caching response headers.
+               $headers = array();
+               foreach ( $page_cache_detail['page_caching_response_headers'] as $page_caching_response_headers ) {
+                       $headers = array_merge( $headers, array_keys( $page_caching_response_headers ) );
+               }
+               $headers = array_unique( $headers );
+
+               // Page cache is detected if there are response headers or a page cache plugin is present.
+               $has_page_caching = ( count( $headers ) > 0 || $page_cache_detail['advanced_cache_present'] );
+
+               if ( $page_speed && $page_speed < $this->get_good_response_time_threshold() ) {
+                       $result = $has_page_caching ? 'good' : 'recommended';
+               } else {
+                       $result = 'critical';
+               }
+
+               return array(
+                       'status'                 => $result,
+                       'advanced_cache_present' => $page_cache_detail['advanced_cache_present'],
+                       'headers'                => $headers,
+                       'response_time'          => $page_speed,
+               );
+       }
+
+       /**
+        * Get the threshold below which a response time is considered good.
+        *
+        * @since 6.1.0
+        *
+        * @return int Threshold in milliseconds.
+        */
+       private function get_good_response_time_threshold() {
+               /**
+                * Filters the threshold below which a response time is considered good.
+                *
+                * The default is based on https://web.dev/time-to-first-byte/.
+                *
+                * @param int $threshold Threshold in milliseconds. Default 600.
+                *
+                * @since 6.1.0
+                */
+               return (int) apply_filters( 'site_status_good_response_time_threshold', 600 );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Determines whether to suggest using a persistent object cache.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 6.1.0
</span></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestsitehealthcontrollerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-site-health-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-site-health-controller.php 2022-08-31 22:17:51 UTC (rev 54042)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-site-health-controller.php   2022-08-31 22:44:04 UTC (rev 54043)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -43,6 +43,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Registers API routes.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.6.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 6.1.0 Adds page-cache async test.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @see register_rest_route()
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -156,6 +157,24 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                },
</span><span class="cx" style="display: block; padding: 0 10px">                        )
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               register_rest_route(
+                       $this->namespace,
+                       sprintf(
+                               '/%s/%s',
+                               $this->rest_base,
+                               'page-cache'
+                       ),
+                       array(
+                               array(
+                                       'methods'             => 'GET',
+                                       'callback'            => array( $this, 'test_page_cache' ),
+                                       'permission_callback' => function () {
+                                               return $this->validate_request_permission( 'view_site_health_checks' );
+                                       },
+                               ),
+                       )
+               );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -243,6 +262,18 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Checks that full page cache is active.
+        *
+        * @since 6.1.0
+        *
+        * @return array The test result.
+        */
+       public function test_page_cache() {
+               $this->load_admin_textdomain();
+               return $this->site_health->get_test_page_cache();
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Gets the current directory sizes for this install.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.6.0
</span></span></pre></div>
<a id="trunktestsphpunittestsrestapirestschemasetupphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php  2022-08-31 22:17:51 UTC (rev 54042)
+++ trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php    2022-08-31 22:44:04 UTC (rev 54043)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -183,6 +183,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        '/wp-site-health/v1/tests/https-status',
</span><span class="cx" style="display: block; padding: 0 10px">                        '/wp-site-health/v1/tests/dotorg-communication',
</span><span class="cx" style="display: block; padding: 0 10px">                        '/wp-site-health/v1/tests/authorization-header',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        '/wp-site-health/v1/tests/page-cache',
</ins><span class="cx" style="display: block; padding: 0 10px">                         '/wp-site-health/v1/directory-sizes',
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunktestsphpunittestsrestapirestsitehealthcontrollerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/rest-api/rest-site-health-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-site-health-controller.php        2022-08-31 22:17:51 UTC (rev 54042)
+++ trunk/tests/phpunit/tests/rest-api/rest-site-health-controller.php  2022-08-31 22:44:04 UTC (rev 54043)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -99,4 +99,49 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $response = rest_do_request( '/wp-site-health/v1/tests/dotorg-communication' );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( 'dotorg_communication', $response->get_data()['test'] );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * Tests Page Cache Rest endpoint registration.
+        *
+        * @ticket 56041
+        */
+       public function test_page_cache_endpoint() {
+               $server = rest_get_server();
+               $routes = $server->get_routes();
+
+               $endpoint = '/wp-site-health/v1/tests/page-cache';
+               $this->assertArrayHasKey( $endpoint, $routes );
+
+               $route = $routes[ $endpoint ];
+               $this->assertCount( 1, $route );
+
+               $route = current( $route );
+               $this->assertEquals(
+                       array( WP_REST_Server::READABLE => true ),
+                       $route['methods']
+               );
+
+               $this->assertEquals(
+                       'test_page_cache',
+                       $route['callback'][1]
+               );
+
+               $this->assertIsCallable( $route['permission_callback'] );
+
+               if ( current_user_can( 'view_site_health_checks' ) ) {
+                       $this->assertTrue( call_user_func( $route['permission_callback'] ) );
+               } else {
+                       $this->assertFalse( call_user_func( $route['permission_callback'] ) );
+               }
+
+               wp_set_current_user( self::factory()->user->create( array( 'role' => 'author' ) ) );
+               $this->assertFalse( call_user_func( $route['permission_callback'] ) );
+
+               $user = wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
+               if ( is_multisite() ) {
+                       // Site health cap is only available for super admins in Multi sites.
+                       grant_super_admin( $user->ID );
+               }
+               $this->assertTrue( call_user_func( $route['permission_callback'] ) );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunktestsphpunittestssitehealthphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/site-health.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/site-health.php 2022-08-31 22:17:51 UTC (rev 54042)
+++ trunk/tests/phpunit/tests/site-health.php   2022-08-31 22:44:04 UTC (rev 54043)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -109,6 +109,99 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @ticket 56041
+        * @dataProvider data_page_cache_test
+        * @covers ::get_test_page_cache()
+        * @covers ::get_page_cache_detail()
+        * @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 ) {
+               $wp_site_health = new WP_Site_Health();
+
+               $expected_props = array(
+                       'badge'  => array(
+                               'label' => __( 'Performance' ),
+                               'color' => 'blue',
+                       ),
+                       'test'   => 'page_cache',
+                       'status' => $expected_status,
+                       'label'  => $expected_label,
+               );
+
+               if ( null !== $good_basic_auth ) {
+                       $_SERVER['PHP_AUTH_USER'] = 'admin';
+                       $_SERVER['PHP_AUTH_PW']   = 'password';
+               }
+
+               $threshold = 10;
+               if ( $delay_the_response ) {
+                       add_filter(
+                               'site_status_good_response_time_threshold',
+                               static function () use ( $threshold ) {
+                                       return $threshold;
+                               }
+                       );
+               }
+
+               add_filter(
+                       'pre_http_request',
+                       function ( $r, $parsed_args ) use ( &$responses, &$is_unauthorized, $good_basic_auth, $delay_the_response, $threshold ) {
+
+                               $expected_response = array_shift( $responses );
+
+                               if ( $delay_the_response ) {
+                                       usleep( $threshold * 1000 + 1 );
+                               }
+
+                               if ( 'unauthorized' === $expected_response ) {
+                                       $is_unauthorized = true;
+
+                                       return array(
+                                               'response' => array(
+                                                       'code'    => 401,
+                                                       'message' => 'Unauthorized',
+                                               ),
+                                       );
+                               }
+
+                               if ( null !== $good_basic_auth ) {
+                                       $this->assertArrayHasKey(
+                                               'Authorization',
+                                               $parsed_args['headers']
+                                       );
+                               }
+
+                               $this->assertIsArray( $expected_response );
+
+                               return array(
+                                       'headers'  => $expected_response,
+                                       'response' => array(
+                                               'code'    => 200,
+                                               'message' => 'OK',
+                                       ),
+                               );
+                       },
+                       20,
+                       2
+               );
+
+               $actual = $wp_site_health->get_test_page_cache();
+               $this->assertArrayHasKey( 'description', $actual );
+               $this->assertArrayHasKey( 'actions', $actual );
+               if ( $is_unauthorized ) {
+                       $this->assertStringContainsString( 'Unauthorized', $actual['description'] );
+               } else {
+                       $this->assertStringNotContainsString( 'Unauthorized', $actual['description'] );
+               }
+
+               $this->assertEquals(
+                       $expected_props,
+                       wp_array_slice_assoc( $actual, array_keys( $expected_props ) )
+               );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * @group ms-excluded
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 56040
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -163,6 +256,158 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Gets response data for get_test_page_cache().
+        * @ticket 56041
+        *
+        * @return array[]
+        */
+       public function data_page_cache_test() {
+               $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';
+               $error_label       = 'Unable to detect the presence of page cache';
+
+               return array(
+                       'basic-auth-fail'                        => array(
+                               'responses'       => array(
+                                       'unauthorized',
+                               ),
+                               'expected_status' => 'recommended',
+                               'expected_label'  => $error_label,
+                               'good_basic_auth' => false,
+                       ),
+                       'no-cache-control'                       => array(
+                               'responses'          => array_fill( 0, 3, array() ),
+                               'expected_status'    => 'critical',
+                               'expected_label'     => $critical_label,
+                               'good_basic_auth'    => null,
+                               'delay_the_response' => true,
+                       ),
+                       'no-cache'                               => array(
+                               'responses'       => array_fill( 0, 3, array( 'cache-control' => 'no-cache' ) ),
+                               'expected_status' => 'recommended',
+                               'expected_label'  => $recommended_label,
+                       ),
+                       'no-cache-arrays'                        => array(
+                               'responses'       => array_fill(
+                                       0,
+                                       3,
+                                       array(
+                                               'cache-control' => array(
+                                                       'no-cache',
+                                                       'no-store',
+                                               ),
+                                       )
+                               ),
+                               'expected_status' => 'recommended',
+                               'expected_label'  => $recommended_label,
+                       ),
+                       'no-cache-with-delayed-response'         => array(
+                               'responses'          => array_fill( 0, 3, array( 'cache-control' => 'no-cache' ) ),
+                               'expected_status'    => 'critical',
+                               'expected_label'     => $critical_label,
+                               'good_basic_auth'    => null,
+                               'delay_the_response' => true,
+                       ),
+                       'age'                                    => array(
+                               'responses'       => array_fill(
+                                       0,
+                                       3,
+                                       array( 'age' => '1345' )
+                               ),
+                               'expected_status' => 'good',
+                               'expected_label'  => $good_label,
+                       ),
+                       'cache-control-max-age'                  => array(
+                               'responses'       => array_fill(
+                                       0,
+                                       3,
+                                       array( 'cache-control' => 'public; max-age=600' )
+                               ),
+                               'expected_status' => 'good',
+                               'expected_label'  => $good_label,
+                       ),
+                       'etag'                                   => array(
+                               'responses'       => array_fill(
+                                       0,
+                                       3,
+                                       array( 'etag' => '"1234567890"' )
+                               ),
+                               'expected_status' => 'good',
+                               'expected_label'  => $good_label,
+                       ),
+                       'cache-control-max-age-after-2-requests' => array(
+                               'responses'       => array(
+                                       array(),
+                                       array(),
+                                       array( 'cache-control' => 'public; max-age=600' ),
+                               ),
+                               'expected_status' => 'good',
+                               'expected_label'  => $good_label,
+                       ),
+                       'cache-control-with-future-expires'      => array(
+                               'responses'       => array_fill(
+                                       0,
+                                       3,
+                                       array( 'expires' => gmdate( 'r', time() + MINUTE_IN_SECONDS * 10 ) )
+                               ),
+                               'expected_status' => 'good',
+                               'expected_label'  => $good_label,
+                       ),
+                       'cache-control-with-past-expires'        => array(
+                               'responses'          => array_fill(
+                                       0,
+                                       3,
+                                       array( 'expires' => gmdate( 'r', time() - MINUTE_IN_SECONDS * 10 ) )
+                               ),
+                               'expected_status'    => 'critical',
+                               'expected_label'     => $critical_label,
+                               'good_basic_auth'    => null,
+                               'delay_the_response' => true,
+                       ),
+                       'cache-control-with-basic-auth'          => array(
+                               'responses'       => array_fill(
+                                       0,
+                                       3,
+                                       array( 'cache-control' => 'public; max-age=600' )
+                               ),
+                               'expected_status' => 'good',
+                               'expected_label'  => $good_label,
+                               'good_basic_auth' => true,
+                       ),
+                       'x-cache-enabled'                        => array(
+                               'responses'       => array_fill(
+                                       0,
+                                       3,
+                                       array( 'x-cache-enabled' => 'true' )
+                               ),
+                               'expected_status' => 'good',
+                               'expected_label'  => $good_label,
+                       ),
+                       'x-cache-enabled-with-delay'             => array(
+                               'responses'          => array_fill(
+                                       0,
+                                       3,
+                                       array( 'x-cache-enabled' => 'false' )
+                               ),
+                               'expected_status'    => 'critical',
+                               'expected_label'     => $critical_label,
+                               'good_basic_auth'    => null,
+                               'delay_the_response' => true,
+                       ),
+                       'x-cache-disabled'                       => array(
+                               'responses'       => array_fill(
+                                       0,
+                                       3,
+                                       array( 'x-cache-disabled' => 'off' )
+                               ),
+                               'expected_status' => 'good',
+                               'expected_label'  => $good_label,
+                       ),
+               );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Data provider.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 56040
</span></span></pre></div>
<a id="trunktestsqunitfixtureswpapigeneratedjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/fixtures/wp-api-generated.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/fixtures/wp-api-generated.js    2022-08-31 22:17:51 UTC (rev 54042)
+++ trunk/tests/qunit/fixtures/wp-api-generated.js      2022-08-31 22:44:04 UTC (rev 54043)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -10615,6 +10615,27 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 ]
</span><span class="cx" style="display: block; padding: 0 10px">             }
</span><span class="cx" style="display: block; padding: 0 10px">         },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        "/wp-site-health/v1/tests/page-cache": {
+            "namespace": "wp-site-health/v1",
+            "methods": [
+                "GET"
+            ],
+            "endpoints": [
+                {
+                    "methods": [
+                        "GET"
+                    ],
+                    "args": []
+                }
+            ],
+            "_links": {
+                "self": [
+                    {
+                        "href": "http://example.org/index.php?rest_route=/wp-site-health/v1/tests/page-cache"
+                    }
+                ]
+            }
+        },
</ins><span class="cx" style="display: block; padding: 0 10px">         "/wp-block-editor/v1": {
</span><span class="cx" style="display: block; padding: 0 10px">             "namespace": "wp-block-editor/v1",
</span><span class="cx" style="display: block; padding: 0 10px">             "methods": [
</span></span></pre>
</div>
</div>

</body>
</html>