PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.4
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.4
1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 All 30 releases
← All changes | includes/class-health.php +128 -0 1.3.11.3.4 View file →
@@ -322,8 +322,62 @@
322 322 );
323 323 }
324 324 }
325 325
326 + /*
327 + * A full-page cache owned by the WEB SERVER, in front of PHP.
328 + *
329 + * Reported only when it is actually there, because it is a fact about
330 + * the host rather than a setting the admin can act on from here — an
331 + * "absent" row would be noise on the ~99% of sites that have no such
332 + * layer. When it IS there it outranks almost everything else on this
333 + * panel: nginx answers before WordPress runs, so what a visitor sees
334 + * is decided by that cache and not by anything xSpeed reports about
335 + * its own.
336 + *
337 + * The severity is about DOUBLE full-page caching, not about the layer
338 + * existing. Two independent full-page caches stacked in front of one
339 + * site have independent TTLs, and the outer one can re-serve HTML the
340 + * inner one has already regenerated — the classic "I purged and it is
341 + * still stale" report. With xSpeed's own page cache off there is only
342 + * one layer and nothing to warn about, so that case is INFO.
343 + */
344 + $host_cache_path = Host_Page_Caches::nginx_helper_cache_path();
345 + if ( null !== $host_cache_path ) {
346 + $detail = $cache_enabled
347 + ? 'Your server is running its own full-page cache in nginx (FastCGI), managed by the Nginx Helper plugin your host installed — so this site has TWO full-page caches stacked in front of it. xSpeed forwards every Purge All to the server layer, but the two expire on their own schedules (the server side is typically an hour), so a page can still be served from nginx after xSpeed has regenerated it. If edits keep looking stale, purge from your host\'s dashboard too, or turn xSpeed\'s page cache off and let the server layer do the work — it is the faster of the two, because it answers before PHP starts.'
348 + : 'Your server is running a full-page cache in nginx (FastCGI), managed by the Nginx Helper plugin your host installed. xSpeed\'s own page cache is off, so this is the only full-page cache in front of the site — and it is the fastest kind, answering before PHP starts. Purge All in xSpeed still clears it.';
349 +
350 + // Path prefix as a fingerprint for the WORDING only — never as a
351 + // gate. Nginx Helper is not xCloud-only; other hosts and manual
352 + // installs use it with a cache directory somewhere else entirely.
353 + if ( 0 === strpos( $host_cache_path, '/etc/nginx/cache/' ) ) {
354 + $detail .= sprintf( ' Cache directory: %s (the layout xCloud provisions).', $host_cache_path );
355 + } else {
356 + $detail .= sprintf( ' Cache directory: %s.', $host_cache_path );
357 + }
358 +
359 + // The purge is a direct unlink by the PHP-FPM user against a
360 + // directory nginx owns. Whether that user can write there is a
361 + // property of the host we cannot test from here without deleting
362 + // someone's cache to find out, so say what to check rather than
363 + // claiming an outcome either way.
364 + if ( 'unlink_files' === Host_Page_Caches::nginx_helper_purge_method() ) {
365 + $detail .= ' The server cache is purged by deleting its files directly, which needs PHP to have write access to that directory — if a purge here never changes what nginx serves, that permission is the thing to check with your host.';
366 + }
367 +
368 + if ( is_multisite() ) {
369 + $detail .= ' On multisite, nginx keys one cache per install rather than per site, so this purge clears every site on the network.';
370 + }
371 +
372 + $out[] = array(
373 + 'id' => 'host_page_cache',
374 + 'tone' => $cache_enabled ? self::WARN : self::INFO,
375 + 'label' => 'Server-level page cache (nginx FastCGI)',
376 + 'detail' => $detail,
377 + );
378 + }
379 +
326 380 // Cache expiry vs preloader schedule (deterministic rule, issue #31):
327 381 // pages that expire faster than the preloader re-warms them leave the
328 382 // cache cold for most real traffic — the classic "24.8% hit ratio with
329 383 // everything on" misconfiguration. Pure logic in
@@ -353,8 +407,16 @@
353 407 ? 'Pretty permalinks active.'
354 408 : 'Set permalinks to anything other than "Plain" — page caching needs URL paths to key on.',
355 409 );
356 410
411 + // What is in front of the site, and what we are telling it. Extracted
412 + // so it can be exercised without paying for every other probe in
413 + // checks(); see edge_check().
414 + $edge_row = self::edge_check();
415 + if ( null !== $edge_row ) {
416 + $out[] = $edge_row;
417 + }
418 +
357 419 // Cache-poisoning Set-Cookie detection (issue #33): a plugin emitting
358 420 // Set-Cookie on anonymous pageviews forces CDN/edge BYPASS for all
359 421 // HTML (Cloudflare never caches a response carrying Set-Cookie). Probe
360 422 // is transient-throttled inside Cookie_Inspector, same pattern as the
@@ -516,8 +578,74 @@
516 578 * wp_get_schedules() lookup.
517 579 * @return array{id:string,tone:string,label:string,detail:string}|null Check
518 580 * row, or null when the rule doesn't apply (preloader off/manual).
519 581 */
582 + /**
583 + * What cache is in front of the site, and what we are telling it.
584 + *
585 + * Reported whether or not anything is currently being held back, because
586 + * the useful half is the caveat rather than the header. A Cloudflare
587 + * Cache Rule set to ignore origin headers overrides everything xSpeed
588 + * sends, and someone debugging "my cart page is still being cached"
589 + * needs telling that rather than left to discover it.
590 + *
591 + * Null when nothing was detected and nothing was switched off: there is
592 + * no news in "we looked and saw nothing", and a row saying so on every
593 + * ordinary single-server site would be noise in a panel people scan for
594 + * problems.
595 + *
596 + * @return array{id:string,tone:string,label:string,detail:string}|null
597 + */
598 + public static function edge_check(): ?array {
599 + $edge = Edge_Provider::detect();
600 +
601 + if ( Edge_Provider::is_off( $edge ) ) {
602 + return array(
603 + 'id' => 'edge_hold',
604 + 'tone' => self::WARN,
605 + 'label' => 'Edge cache not being told anything',
606 + 'detail' => 'xSpeed is set not to send cache headers to the CDN in front of this site, so first renders and bypassed pages can be stored at the edge. Set "Cache In Front Of This Site" back to automatic unless you are sending your own headers.',
607 + );
608 + }
609 +
610 + if ( Edge_Provider::NONE === $edge['confidence'] ) {
611 + return null;
612 + }
613 +
614 + $named = '' !== $edge['provider'] ? $edge['provider'] : 'a cache we could not identify';
615 +
616 + // A pin outranks detection by design, so nothing re-checks it on the
617 + // site's behalf — and it is the one answer that also reaches the
618 + // drop-in and the server rules. Comparing it against the request is
619 + // the only way a site that changed CDN ever finds out.
620 + $sniffed = Edge_Provider::sniffed();
621 + if ( in_array( $edge['source'], array( 'setting', 'constant', 'filter' ), true )
622 + && '' !== $sniffed['provider']
623 + && $sniffed['provider'] !== $edge['provider'] ) {
624 + return array(
625 + 'id' => 'edge_hold',
626 + 'tone' => self::WARN,
627 + 'label' => 'Edge cache setting looks out of date',
628 + 'detail' => sprintf(
629 + 'This request looks like %s, but the provider is pinned to %s. If the site moved, update it — the pinned answer is also baked into the drop-in and the server rules.',
630 + $sniffed['provider'],
631 + $named
632 + ),
633 + );
634 + }
635 +
636 + $caveat = 'cloudflare' === $edge['provider']
637 + ? ' A Cloudflare Cache Rule whose Edge TTL is "Ignore cache-control header and use this TTL" overrides this; use "Respect origin TTL" on that rule.'
638 + : '';
639 +
640 + return array(
641 + 'id' => 'edge_hold',
642 + 'tone' => self::OK,
643 + 'label' => 'Edge cache being told what not to store',
644 + 'detail' => sprintf( 'First renders, bypassed pages and mobile-split pages are marked do-not-store for %s.%s', $named, $caveat ),
645 + );
646 + }
647 +
520 648 public static function expiry_preload_check( int $expiry_hours, string $schedule, bool $preloader_enabled, ?int $interval_hours = null ): ?array {
521 649 if ( ! $preloader_enabled ) {
522 650 return null;
523 651 }