| @@ -75,8 +75,52 @@ | ||
| 75 | 75 | * embedded when nginx is detected. Skipped entirely when cache is |
| 76 | 76 | * disabled — no point telling the user to install a rewrite they |
| 77 | 77 | * haven't opted into. |
| 78 | 78 | */ |
| 79 | + /** | |
| 80 | + * Decide what the nginx static rewrite is actually doing. | |
| 81 | + * | |
| 82 | + * Extracted so the ordering is testable without a WordPress bootstrap, | |
| 83 | + * and so Site Health and the dashboard Health panel cannot drift apart | |
| 84 | + * again — the whole point of #480. | |
| 85 | + * | |
| 86 | + * Returns one of: 'active', 'mobile_separate', 'skipped_nonce', | |
| 87 | + * 'unverified', 'required'. | |
| 88 | + * | |
| 89 | + * @param array<string, mixed> $probe probe_static_rewrite() result. | |
| 90 | + * @param string $block_reason A known refusal, or ''. | |
| 91 | + */ | |
| 92 | + public static function nginx_rewrite_verdict( array $probe, string $block_reason ): string { | |
| 93 | + $is_active = (bool) ( $probe['active'] ?? false ); | |
| 94 | + $inconclusive = (bool) ( $probe['inconclusive'] ?? false ); | |
| 95 | + | |
| 96 | + // A known refusal OUTRANKS the probe. probe_static_rewrite() writes | |
| 97 | + // its own file under the static-cache dir and fetches that, which | |
| 98 | + // succeeds whenever the server can serve a static file at all — even | |
| 99 | + // when no real page is on the static path. It also outranks | |
| 100 | + // "inconclusive", so a blocked rewrite whose probe merely failed to | |
| 101 | + // complete is reported as the refusal it is. (FBS-83145) | |
| 102 | + if ( '' !== $block_reason ) { | |
| 103 | + if ( 'mobile_separate' === $block_reason ) { | |
| 104 | + return 'mobile_separate'; | |
| 105 | + } | |
| 106 | + if ( 'skipped_nonce' === $block_reason ) { | |
| 107 | + return 'skipped_nonce'; | |
| 108 | + } | |
| 109 | + return 'required'; | |
| 110 | + } | |
| 111 | + | |
| 112 | + if ( $is_active ) { | |
| 113 | + return 'active'; | |
| 114 | + } | |
| 115 | + | |
| 116 | + // The probe never reached a verdict (blocked loopback, self-signed | |
| 117 | + // cert, timeout, a CDN/WAF answering instead of the origin). That is | |
| 118 | + // not evidence the config is wrong, and must not produce a | |
| 119 | + // "paste this snippet" banner. (FBS-84012, #480) | |
| 120 | + return $inconclusive ? 'unverified' : 'required'; | |
| 121 | + } | |
| 122 | + | |
| 79 | 123 | public function site_status_static_rewrite(): array { |
| 80 | 124 | $result = array( |
| 81 | 125 | 'label' => __( 'xSpeed static-rewrite cache is active', 'xspeed' ), |
| 82 | 126 | 'status' => 'good', |
| @@ -120,8 +164,62 @@ | ||
| 120 | 164 | return $result; |
| 121 | 165 | } |
| 122 | 166 | |
| 123 | 167 | if ( \XSpeed\Server::NGINX === $server_type ) { |
| 168 | + // Ask the same question the dashboard Health panel asks, the same | |
| 169 | + // way. This test used to return "config required" unconditionally, | |
| 170 | + // so every correctly-configured nginx site — every xCloud site, | |
| 171 | + // where the panel installs the block for you — was told to paste a | |
| 172 | + // snippet it already had, and re-running the check never cleared | |
| 173 | + // it. Worse, the dashboard said the opposite at the same moment. | |
| 174 | + // Reuse probe_static_rewrite() + the refusal reasons so the two | |
| 175 | + // surfaces cannot disagree. (#480) | |
| 176 | + $probe = \XSpeed\Cache::probe_static_rewrite( true ); | |
| 177 | + $probe_reason = (string) ( $probe['reason'] ?? '' ); | |
| 178 | + | |
| 179 | + $block_reason = \XSpeed\Cache::static_rewrite_block_reason(); | |
| 180 | + $skip = \XSpeed\Cache::last_static_skip(); | |
| 181 | + if ( '' === $block_reason && ! empty( $skip['reason'] ) ) { | |
| 182 | + $block_reason = 'skipped_' . (string) $skip['reason']; | |
| 183 | + } | |
| 184 | + | |
| 185 | + switch ( self::nginx_rewrite_verdict( $probe, $block_reason ) ) { | |
| 186 | + case 'active': | |
| 187 | + $result['label'] = __( 'xSpeed nginx static rewrite is active', 'xspeed' ); | |
| 188 | + $result['status'] = 'good'; | |
| 189 | + $result['description'] = '<p>' . esc_html__( 'nginx is serving cache hits directly — PHP is bypassed (~5-15ms TTFB). No action needed.', 'xspeed' ) . '</p>'; | |
| 190 | + return $result; | |
| 191 | + | |
| 192 | + case 'mobile_separate': | |
| 193 | + $result['label'] = __( 'xSpeed static rewrite is off (Separate Mobile Cache)', 'xspeed' ); | |
| 194 | + $result['status'] = 'recommended'; | |
| 195 | + $result['description'] = '<p>' . esc_html__( 'Separate Mobile Cache is on, so cache hits are served by the PHP drop-in to keep per-device HTML correct. Turn Separate Mobile Cache off if your site serves the same HTML to every device to regain the faster static path.', 'xspeed' ) . '</p>'; | |
| 196 | + return $result; | |
| 197 | + | |
| 198 | + case 'skipped_nonce': | |
| 199 | + $result['label'] = __( 'xSpeed is serving cache hits through PHP (pages contain nonces)', 'xspeed' ); | |
| 200 | + $result['status'] = 'recommended'; | |
| 201 | + $result['description'] = '<p>' . esc_html__( 'Your nginx config is correct, but pages are not reaching the static cache, so hits are served by PHP. They contain nonces, and a static file is served with no PHP — nothing could ever refresh them, so every anonymous form on the page would break once they expire. Keeping these pages on PHP is deliberate.', 'xspeed' ) . '</p>'; | |
| 202 | + return $result; | |
| 203 | + | |
| 204 | + case 'unverified': | |
| 205 | + // The probe never reached a verdict (blocked loopback, | |
| 206 | + // self-signed cert, timeout, a CDN/WAF answering instead of | |
| 207 | + // the origin). Not evidence the config is wrong, so don't | |
| 208 | + // say "required" and don't dump a snippet the user has | |
| 209 | + // probably already pasted. (FBS-84012, and why #480 was filed.) | |
| 210 | + $result['label'] = __( 'xSpeed could not verify the nginx static rewrite', 'xspeed' ); | |
| 211 | + $result['status'] = 'recommended'; | |
| 212 | + $result['description'] = '<p>' . esc_html( | |
| 213 | + sprintf( | |
| 214 | + /* translators: %s: the reason the probe could not complete. */ | |
| 215 | + __( 'The check itself did not complete, so this is not evidence that your config is wrong — if you have already pasted the snippet it may well be working. Reason: %s', 'xspeed' ), | |
| 216 | + $probe_reason | |
| 217 | + ) | |
| 218 | + ) . '</p>'; | |
| 219 | + return $result; | |
| 220 | + } | |
| 221 | + | |
| 124 | 222 | $snippet = \XSpeed\Cache::nginx_snippet(); |
| 125 | 223 | $result['label'] = __( 'xSpeed nginx server config required', 'xspeed' ); |
| 126 | 224 | $result['status'] = 'recommended'; |
| 127 | 225 | $result['description'] = '<p>' . esc_html__( 'xSpeed can\'t write nginx config from PHP. Paste this snippet into your site\'s server { } block, then reload nginx so cache hits serve without booting PHP:', 'xspeed' ) . '</p>' |