PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.6
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.6
1.3.6 1.3.5 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 All 32 releases
← All changes | includes/class-cache.php +111 -10 1.3.3 → 1.3.6 View file →
@@ -3481,8 +3481,16 @@
3481 3481 * markup across the site, so they require a site response-cache purge.
3482 3482 * Content saves also require this scope while their local operation is a
3483 3483 * complete bucket sweep.
3484 3484 *
3485 + * A new term is `content`, not `presentation`. It has no posts yet, so no
3486 + * page renders it until a post is saved with it, and that save is its own
3487 + * content purge. Classed as presentation, it cleared the host's whole
3488 + * nginx cache every time a post was published with a tag that did not
3489 + * exist yet, which is most publishing. Renaming or deleting a term stays
3490 + * presentation: the new name shows on every post in the term, and Nginx
3491 + * Helper purges only the homepage for either. (QA #448)
3492 + *
3485 3493 * @return array{scope:string,intent:string,urls:array<int,string>}
3486 3494 */
3487 3495 private static function invalidation_for_hook( string $hook ): array {
3488 3496 $presentation = array(
@@ -3488,9 +3496,8 @@
3488 3496 $presentation = array(
3489 3497 'switch_theme',
3490 3498 'activated_plugin',
3491 3499 'deactivated_plugin',
3492 - 'created_term',
3493 3500 'edited_term',
3494 3501 'delete_term',
3495 3502 'wp_update_nav_menu',
3496 3503 );
@@ -5561,8 +5568,25 @@
5561 5568 // True when an edge cache (Cloudflare) fronts the origin, so hits are
5562 5569 // absorbed before reaching PHP. The dashboard labels the ratio
5563 5570 // "origin-layer only" instead of implying it's the full picture. (#118)
5564 5571 'edge_cache' => self::edge_cache_detected(),
5572 + // LiteSpeed Static Fast Path (#509): the web server serves hits
5573 + // with no PHP, no way to tag them, and no way to count them. The
5574 + // dashboard labels the ratio as PHP-layer only so a low number
5575 + // reads as the trade the user chose, not a fault.
5576 + //
5577 + // rewrite_installed() is part of the condition (QA on #513): when
5578 + // the .htaccess write failed (read-only file), hits still take
5579 + // the drop-in path and ARE counted — the disclosure would be the
5580 + // opposite of the truth. Health carries the "block missing"
5581 + // warning for that state; this flag only speaks when static
5582 + // serving is genuinely in effect.
5583 + 'static_hits_uncounted' => (
5584 + Server::LITESPEED === Server::type()
5585 + && ! empty( Settings::get()['cache_enabled'] )
5586 + && self::static_rewrite_allowed()
5587 + && self::rewrite_installed()
5588 + ),
5565 5589 /*
5566 5590 * Whether the page cache is actually SERVING, as opposed to
5567 5591 * switched on in settings. The hero read the setting alone and
5568 5592 * announced "Active — serving cached HTML"; a site whose
@@ -6471,8 +6495,28 @@
6471 6495 $stored = get_option( Settings_Manager::OPTION_PREFIX . 'cache', array() );
6472 6496 return is_array( $stored ) ? $stored : array();
6473 6497 }
6474 6498
6499 + /**
6500 + * Strict truthiness for the LiteSpeed Static Fast Path opt-in.
6501 + *
6502 + * On non-LiteSpeed servers the key is out of the schema and carried by
6503 + * preserved_keys(), so a REST/MCP write lands VERBATIM — QA on #513
6504 + * stored the string "false" on Apache and the fast path installed
6505 + * itself the moment the site moved to LiteSpeed, because
6506 + * empty("false") is false. Only an explicit, unambiguous "yes" may
6507 + * enable a path that trades away hit tagging; any other value —
6508 + * "false", "no", arbitrary junk — stays OFF, which is the default the
6509 + * user never left.
6510 + */
6511 + private static function litespeed_optin_enabled( $value ): bool {
6512 + if ( true === $value || 1 === $value ) {
6513 + return true;
6514 + }
6515 + return is_string( $value )
6516 + && in_array( strtolower( trim( $value ) ), array( '1', 'true', 'on', 'yes' ), true );
6517 + }
6518 +
6475 6519 public static function sync_mobile_flag( $enabled = null ): void {
6476 6520 if ( null === $enabled ) {
6477 6521 $stored = self::stored_cache_opts();
6478 6522 $enabled = ! empty( $stored['mobile_separate'] );
@@ -6645,12 +6689,24 @@
6645 6689 * the truth there. (Apache keeps the static fast path — it honors the
6646 6690 * header.) See maybe_emit_lscache_headers() for the paired LSCache
6647 6691 * stand-down that stops LiteSpeed's own module from shadowing the
6648 6692 * drop-in.
6693 + *
6694 + * Opt-in (#509): `litespeed_static_rewrite` re-enables the fast path on
6695 + * LiteSpeed for users who value raw TTFB over hit accounting. The trade
6696 + * is stated in the setting's copy: statically served hits carry no
6697 + * X-XSpeed-Cache header and are not counted (LiteSpeed logs the
6698 + * original request line, so even the access-log scan cannot see
6699 + * them — see Hit_Counter::collect_server_log_hits()). The drop-in
6700 + * default above stays — nobody is surprised into an unverifiable cache.
6649 6701 */
6650 6702 public static function static_rewrite_allowed(): bool {
6651 - // LiteSpeed: drop-in serves hits (visible + counted) — see docblock.
6652 - if ( Server::LITESPEED === Server::type() ) {
6703 + // Stored read — reached from boot(); see stored_cache_opts().
6704 + $opts = self::stored_cache_opts();
6705 + // LiteSpeed: drop-in serves hits (visible + counted) unless the user
6706 + // explicitly opted into the static fast path — see docblock.
6707 + if ( Server::LITESPEED === Server::type()
6708 + && ! self::litespeed_optin_enabled( $opts['litespeed_static_rewrite'] ?? false ) ) {
6653 6709 return false;
6654 6710 }
6655 6711 // Apache without mod_headers is in EXACTLY the position LiteSpeed
6656 6712 // is in above: it can run the RewriteRule and serve the static
@@ -6664,10 +6720,8 @@
6664 6720 // pinned at 0% on a working Apache cache.)
6665 6721 if ( Server::APACHE === Server::type() && ! Server::apache_has_mod_headers() ) {
6666 6722 return false;
6667 6723 }
6668 - // Stored read — reached from boot(); see stored_cache_opts().
6669 - $opts = self::stored_cache_opts();
6670 6724 return empty( $opts['mobile_separate'] );
6671 6725 }
6672 6726
6673 6727 /**
@@ -6785,8 +6839,10 @@
6785 6839 case 'mobile_separate':
6786 6840 return 'Separate Mobile Cache is on, which disables the device-blind static rewrite. Cache hits are served by PHP instead. If your site serves the same HTML to every device, turn it off in Cache settings for much faster hits.';
6787 6841 case 'no_mod_headers':
6788 6842 return "Apache's mod_headers is not loaded, so the static rewrite cannot mark its responses as cache hits. Enable mod_headers, or leave hits on the PHP path.";
6843 + case 'litespeed_dropin':
6844 + return 'On LiteSpeed, cache hits are served by the PHP drop-in so every hit is tagged X-XSpeed-Cache and counted in the hit ratio — LiteSpeed\'s .htaccess engine cannot do either for statically served files. If raw TTFB matters more to you than hit accounting, turn on LiteSpeed Static Fast Path in Cache settings to serve hits straight from the web server.';
6789 6845 case 'skipped_nonce':
6790 6846 return 'The server config is correct, but pages are not reaching the static cache because they contain nonces, so hits are served by PHP instead. A static file is served with no PHP, so a nonce baked into one could never be refreshed and every anonymous form on the page would break once it expired — keeping these pages on PHP is deliberate. Nonces usually come from plugin widgets; disabling the ones the site does not use lets its pages be served statically again.';
6791 6847 default:
6792 6848 return sprintf( 'The static rewrite is disabled (%s).', $code );
@@ -6805,9 +6861,25 @@
6805 6861 if ( empty( $opts['cache_enabled'] ) ) {
6806 6862 return '';
6807 6863 }
6808 6864 if ( Server::LITESPEED === Server::type() ) {
6809 - return ''; // Intended on LiteSpeed — not a "block".
6865 + // The opt-in is read RAW (stored_cache_opts), not through
6866 + // Settings_Manager::get(): the schema's bool coercion is a PHP
6867 + // cast, and (bool) "false" is true — so a junk string stored on
6868 + // another server (where the key bypasses the schema) would come
6869 + // back from the coercion layer as an ENABLE. Raw + the strict
6870 + // parse below is the same read static_rewrite_allowed() makes,
6871 + // so the two can't disagree either. (QA on #513)
6872 + $stored = self::stored_cache_opts();
6873 + // The intended default — but no longer silent: with the opt-in
6874 + // off, Health must be able to explain the PHP path and point at
6875 + // the toggle instead of falling through to "reinstall the block"
6876 + // advice that cannot work here. (#509)
6877 + if ( ! self::litespeed_optin_enabled( $stored['litespeed_static_rewrite'] ?? false ) ) {
6878 + return 'litespeed_dropin';
6879 + }
6880 + $cache_opts = Settings_Manager::get( 'cache' );
6881 + return ! empty( $cache_opts['mobile_separate'] ) ? 'mobile_separate' : '';
6810 6882 }
6811 6883 if ( Server::APACHE === Server::type() && ! Server::apache_has_mod_headers() ) {
6812 6884 return 'no_mod_headers';
6813 6885 }
@@ -7675,9 +7747,12 @@
7675 7747
7676 7748 $home = home_url( '/' );
7677 7749 $host = (string) wp_parse_url( $home, PHP_URL_HOST );
7678 7750 if ( '' === $host ) {
7679 - $result = array( 'active' => false, 'reason' => 'home_url has no host' );
7751 + // Environmental failure, not evidence the server config is wrong —
7752 + // mark it inconclusive so Health surfaces say "could not verify"
7753 + // instead of demanding a snippet paste. (#480)
7754 + $result = array( 'active' => false, 'inconclusive' => true, 'reason' => 'home_url has no host' );
7680 7755 set_transient( 'xspeed_rewrite_probe', $result, MINUTE_IN_SECONDS );
7681 7756 return $result;
7682 7757 }
7683 7758
@@ -7694,9 +7769,12 @@
7694 7769 if ( ! file_exists( $probe_dir ) ) {
7695 7770 wp_mkdir_p( $probe_dir );
7696 7771 }
7697 7772 if ( ! is_dir( $probe_dir ) ) {
7698 - $result = array( 'active' => false, 'reason' => 'cannot create probe dir' );
7773 + // A cache-dir permissions problem — the probe never ran, so this
7774 + // says nothing about the nginx config. Inconclusive, not
7775 + // "required". (#480)
7776 + $result = array( 'active' => false, 'inconclusive' => true, 'reason' => 'cannot create probe dir' );
7699 7777 set_transient( 'xspeed_rewrite_probe', $result, MINUTE_IN_SECONDS );
7700 7778 return $result;
7701 7779 }
7702 7780 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_put_contents_file_put_contents -- WP_Filesystem requires admin credentials we may not have here; the file is in our own cache dir.
@@ -8814,11 +8892,34 @@
8814 8892 // Only honour known types; anything else falls back to a full purge.
8815 8893 if ( ! array_key_exists( $type, self::purge_types() ) ) {
8816 8894 $type = 'all';
8817 8895 }
8896 +
8897 + // Answer the browser BEFORE purging. "Purge All" fans out to the local
8898 + // sweep, the object cache, CSS/edge listeners (outbound HTTP) and
8899 + // third-party render caches, all in this one request — on a large site
8900 + // that can outlive PHP-FPM's request_terminate_timeout, FPM kills the
8901 + // worker mid-purge, and nginx answers the admin's click with a 502.
8902 + // fastcgi_finish_request() exists on exactly those FPM setups: send
8903 + // the redirect, close the connection, then keep purging in the same
8904 + // process. Elsewhere (mod_php, CLI tests) fall back to purge-then-
8905 + // redirect as before.
8906 + $redirect = self::safe_purge_redirect( wp_get_referer() );
8907 + if ( function_exists( 'ignore_user_abort' ) ) {
8908 + ignore_user_abort( true );
8909 + }
8910 + if ( function_exists( 'set_time_limit' ) ) {
8911 + @set_time_limit( 300 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- best-effort under safe-mode-like restrictions.
8912 + }
8913 + if ( function_exists( 'fastcgi_finish_request' ) ) {
8914 + wp_safe_redirect( $redirect );
8915 + fastcgi_finish_request();
8916 + self::purge_type( $type );
8917 + exit;
8918 + }
8919 +
8818 8920 self::purge_type( $type );
8819 -
8820 - wp_safe_redirect( self::safe_purge_redirect( wp_get_referer() ) );
8921 + wp_safe_redirect( $redirect );
8821 8922 exit;
8822 8923 }
8823 8924
8824 8925 /**