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/modules/Cache/CacheModule.php +166 -5 1.3.0 → 1.3.6 View file →
@@ -145,9 +145,9 @@
145 145 return array();
146 146 }
147 147
148 148 public function settings_schema(): array {
149 - return array(
149 + $schema = array(
150 150 'cache_expiry' => array(
151 151 'type' => 'int',
152 152 // Matches the wizard's Balanced preset, which is what a fresh
153 153 // install starts on — a shorter module default meant the two
@@ -170,9 +170,11 @@
170 170 '/xmlrpc.php',
171 171 '~wp-.*\.php',
172 172 '/feed/',
173 173 'index.php',
174 - '~sitemap(_index)?\.xml',
174 + // `sitemaps?` — SEOPress generates sitemaps.xml (plural).
175 + '~sitemaps?(_index)?\.xml',
176 + '/robots.txt',
175 177 // Bare (no trailing slash) so "contains" matches both
176 178 // /cart and /cart/items — WooCommerce serves both forms.
177 179 '/cart',
178 180 '/checkout',
@@ -228,9 +230,73 @@
228 230 'default' => false,
229 231 'label' => __( 'Separate Mobile Cache', 'xspeed' ),
230 232 'description' => __( 'Keep mobile and desktop responses in separate cache buckets. Turn on for AMP, mobile-specific themes (WPtouch / Jetpack mobile theme), or any setup that serves different HTML by device.', 'xspeed' ),
231 233 ),
234 + 'edge_provider' => array(
235 + 'type' => 'enum',
236 + 'default' => 'auto',
237 + 'options' => array( 'auto', 'off', 'cloudflare', 'fastly', 'varnish', 'nginx', 'akamai', 'cloudfront', 'google', 'keycdn', 'bunny', 'sucuri', 'incapsula', 'generic', 'custom' ),
238 + 'option_labels' => array(
239 + 'auto' => 'Detect automatically',
240 + 'off' => 'Off — send nothing',
241 + 'cloudflare' => 'Cloudflare',
242 + 'varnish' => 'Varnish',
243 + 'nginx' => 'nginx proxy cache',
244 + 'cloudfront' => 'Amazon CloudFront',
245 + 'google' => 'Google Cloud CDN',
246 + 'keycdn' => 'KeyCDN',
247 + 'bunny' => 'Bunny',
248 + // These four cannot be presented as supported on the same
249 + // footing as the ones above. Vendor documentation either
250 + // does not establish that they honour what we send, or
251 + // establishes that they ignore origin cache headers until
252 + // the property is configured to respect them — Akamai
253 + // caches for a theoretically infinite time by default, and
254 + // Sucuri's default caching level ignores the headers
255 + // outright. Naming them without the caveat would promise a
256 + // protection the CDN is not currently giving.
257 + 'fastly' => 'Fastly (needs CDN configuration)',
258 + 'akamai' => 'Akamai (needs CDN configuration)',
259 + 'sucuri' => 'Sucuri (needs CDN configuration)',
260 + 'incapsula' => 'Imperva / Incapsula (needs CDN configuration)',
261 + 'generic' => 'Something else',
262 + 'custom' => 'Custom headers',
263 + ),
264 + 'label' => __( 'Cache In Front Of This Site', 'xspeed' ),
265 + 'description' => __( 'Ask a CDN or proxy in front of your site not to store pages xSpeed refused to cache. Leave it on Detect automatically unless you know what is in front of you; the marked providers ignore origin headers until you configure them to respect it.', 'xspeed' ),
266 + 'info_title' => __( 'Cache in front of this site', 'xspeed' ),
267 + 'info' => __( 'Naming your provider narrows the headers to the one it reads. Detect automatically works it out per request and otherwise sends a set every cache ignores unless it understands it, so it is safe not to know. Run "wp xspeed cache edge" to see what was detected and what gets sent.', 'xspeed' )
268 + ),
269 + 'edge_custom_headers' => array(
270 + 'type' => 'list',
271 + 'default' => array(),
272 + 'item_type' => 'string',
273 + 'label' => __( 'Custom Edge Headers', 'xspeed' ),
274 + 'dependsOn' => array( 'field' => 'edge_provider', 'value' => 'custom' ),
275 + 'description' => __( 'One header per line, as Name: value — for example "Surrogate-Control: no-store". Lines starting with # are ignored.', 'xspeed' ),
276 + 'info_title' => __( 'Custom edge headers', 'xspeed' ),
277 + 'info' => __( 'These replace the headers xSpeed would have picked for your CDN. Two baselines are still added underneath: a Cache-Control, and "X-Accel-Expires: 0" for a page cache running in nginx on your own server. Name either one yourself and yours is used instead. Values containing $, % or a backslash are dropped — the same pairs go into nginx and Apache directives, where those cannot be escaped safely. Content-Length, Content-Encoding, Content-Type, Transfer-Encoding, Set-Cookie and Location are refused.', 'xspeed' )
278 + ),
232 279 );
280 +
281 + // LiteSpeed-only opt-in (#509): meaningless on any other server, so
282 + // the field only exists in the schema where it can act — elsewhere the
283 + // stored value survives via preserved_keys(). Inserted right after
284 + // mobile_separate, its sibling static-fast-path trade-off.
285 + if ( \XSpeed\Server::LITESPEED === \XSpeed\Server::type() ) {
286 + $litespeed = array(
287 + 'litespeed_static_rewrite' => array(
288 + 'type' => 'bool',
289 + 'default' => false,
290 + 'label' => __( 'LiteSpeed Static Fast Path', 'xspeed' ),
291 + 'description' => __( 'Serve cache hits straight from the web server via .htaccess instead of the PHP drop-in. No PHP runs on a hit, so the saving depends on how quickly PHP answers on this host — a few milliseconds on a fast server, far more where PHP is the bottleneck. The trade: LiteSpeed cannot add the X-XSpeed-Cache header to statically served pages, and those hits are not counted in the dashboard hit ratio. Leave off to keep every hit visibly tagged and counted.', 'xspeed' ),
292 + ),
293 + );
294 + $pos = (int) array_search( 'mobile_separate', array_keys( $schema ), true ) + 1;
295 + $schema = array_slice( $schema, 0, $pos, true ) + $litespeed + array_slice( $schema, $pos, null, true );
296 + }
297 +
298 + return $schema;
233 299 }
234 300
235 301 /**
236 302 * `mobile_separate_review` lives outside the schema: migration sets it
@@ -243,9 +309,18 @@
243 309 *
244 310 * @return string[]
245 311 */
246 312 public function preserved_keys(): array {
247 - return array( 'mobile_separate_review' );
313 + $keys = array( 'mobile_separate_review' );
314 + // On non-LiteSpeed servers the litespeed_static_rewrite field is not
315 + // in the schema (see settings_schema()), so a schema-driven save
316 + // would silently drop a value chosen while the site ran LiteSpeed.
317 + // Preserve it so moving LiteSpeed → other → LiteSpeed keeps the
318 + // user's choice. On LiteSpeed itself the schema owns the key.
319 + if ( \XSpeed\Server::LITESPEED !== \XSpeed\Server::type() ) {
320 + $keys[] = 'litespeed_static_rewrite';
321 + }
322 + return $keys;
248 323 }
249 324
250 325 /**
251 326 * Seed per-module option from the legacy xspeed_options blob if we
@@ -416,14 +491,14 @@
416 491 ),
417 492 array(
418 493 'name' => 'xspeed cache',
419 494 'callback' => array( $this, 'cli_handler' ),
420 - 'shortdesc' => 'Inspect the Cache module: `status` (settings), `inventory` (which pages are cached, and how old), `size` (where the disk usage goes), `purge-log` (what cleared the cache, when and why), `purge-url <url>` to clear one page, `recheck-rewrite` to re-run the static-rewrite probe, or `nginx-config` to print the unified nginx server-block for pasting into a vhost. To clear the whole site use `wp xspeed purge`.',
495 + 'shortdesc' => 'Inspect the Cache module: `status` (settings), `inventory` (which pages are cached, and how old), `size` (where the disk usage goes), `purge-log` (what cleared the cache, when and why), `purge-url <url>` to clear one page, `recheck-rewrite` to re-run the static-rewrite probe, or `nginx-config` to print the unified nginx server-block for pasting into a vhost, or `edge` to show which cache is in front of the site and what xSpeed tells it. To clear the whole site use `wp xspeed purge`.',
421 496 'synopsis' => array(
422 497 array(
423 498 'type' => 'positional',
424 499 'name' => 'action',
425 - 'options' => array( 'status', 'inventory', 'size', 'purge-log', 'purge-url', 'recheck-rewrite', 'nginx-config' ),
500 + 'options' => array( 'status', 'inventory', 'size', 'purge-log', 'purge-url', 'recheck-rewrite', 'nginx-config', 'edge' ),
426 501 'optional' => true,
427 502 ),
428 503 array(
429 504 'type' => 'positional',
@@ -832,13 +907,99 @@
832 907 $this->cli_purge_log( $limit );
833 908 return;
834 909 }
835 910
911 + if ( 'edge' === $action ) {
912 + $this->cli_edge();
913 + return;
914 + }
915 +
836 916 $opts = Settings_Manager::get( self::SLUG );
837 917 \WP_CLI::log( 'cache_expiry ' . $opts['cache_expiry'] . 'h' );
838 918 \WP_CLI::log( 'excluded_urls ' . count( $opts['excluded_urls'] ) . ' entries' );
839 919 foreach ( $opts['excluded_urls'] as $u ) {
840 920 \WP_CLI::log( ' - ' . $u );
921 + }
922 + $edge = \XSpeed\Edge_Provider::detect();
923 + \WP_CLI::log( 'edge ' . ( '' !== $edge['provider'] ? $edge['provider'] : $edge['confidence'] ) . ' (' . $edge['source'] . ')' );
924 + }
925 +
926 + /**
927 + * `wp xspeed cache edge` — what we think is in front, and what we say to it.
928 + *
929 + * Worth printing even when nothing is held back. "You are behind
930 + * Cloudflare, and a Cache Rule set to ignore origin headers overrides
931 + * anything xSpeed sends" is the answer to a support question that
932 + * otherwise costs someone a week, and it is true whether or not a hold
933 + * ever fires.
934 + */
935 + private function cli_edge(): void {
936 + $answer = \XSpeed\Edge_Provider::detect();
937 +
938 + \WP_CLI::log( 'provider ' . ( '' !== $answer['provider'] ? $answer['provider'] : '(none named)' ) );
939 + \WP_CLI::log( 'confidence ' . $answer['confidence'] );
940 + \WP_CLI::log( 'source ' . $answer['source'] );
941 +
942 + // A pin outranks detection by design, so nothing re-checks it on the
943 + // site's behalf. Saying the two disagree is the whole mechanism by
944 + // which a site that changed CDN ever finds out.
945 + $sniffed = \XSpeed\Edge_Provider::sniffed();
946 + if ( in_array( $answer['source'], array( 'setting', 'constant', 'filter' ), true )
947 + && '' !== $sniffed['provider']
948 + && $sniffed['provider'] !== $answer['provider'] ) {
949 + \WP_CLI::warning(
950 + sprintf(
951 + 'This request looks like %s, but the provider is pinned to %s. If the site moved, change it — the pinned answer is also baked into the drop-in and the server rules.',
952 + $sniffed['provider'],
953 + '' !== $answer['provider'] ? $answer['provider'] : 'off'
954 + )
955 + );
956 + }
957 +
958 + if ( \XSpeed\Edge_Provider::is_off( $answer ) ) {
959 + \WP_CLI::log( '' );
960 + \WP_CLI::log( 'Nothing is sent: this is switched off.' );
961 + return;
962 + }
963 +
964 + // Resolved through edge_headers_for() rather than straight off the
965 + // provider, so this prints what the serve path would ACTUALLY send —
966 + // including `X-XSpeed-Edge-Hold`, and including the evidence gate.
967 + // Listing the provider's raw set ignored that gate and told operators
968 + // a first render would be held on a site where it would not be.
969 + //
970 + // `bake`, not `request`. Two reasons, and the second one matters:
971 + // this command answers for the site rather than for one response, and
972 + // `request` fires `xspeed_edge_optimization_pending`, whose Pro
973 + // listener resolves the CSS plan — which by its own description is
974 + // what queues a build. A read-only command must not burn a build
975 + // slot, quarantine an entry or purge a page just by being run, and
976 + // under WP-CLI it would do all three against the home page.
977 + $bypass = \XSpeed\Cache::edge_headers_for( 'BYPASS', 'bake', 'logged-in' );
978 + $miss = \XSpeed\Cache::edge_headers_for( 'MISS', 'bake' );
979 +
980 + \WP_CLI::log( '' );
981 + \WP_CLI::log( 'On a page xSpeed refuses to cache (a cart, a logged-in view):' );
982 + foreach ( $bypass as $name => $value ) {
983 + \WP_CLI::log( sprintf( ' %s: %s', $name, $value ) );
984 + }
985 +
986 + \WP_CLI::log( '' );
987 + if ( array() === $miss ) {
988 + \WP_CLI::log( 'On a first render: nothing. A MISS is a performance hedge, so it is held only where a cache in front was detected — and none was. Name the provider in Cache In Front Of This Site to cover first renders too.' );
989 + } else {
990 + \WP_CLI::log( 'On a first render:' );
991 + foreach ( $miss as $name => $value ) {
992 + \WP_CLI::log( sprintf( ' %s: %s', $name, $value ) );
993 + }
994 + }
995 +
996 + \WP_CLI::log( '' );
997 + \WP_CLI::log( 'X-XSpeed-Edge-Hold names why a response was held: bypass, bypass-shape, miss, mobile-split or pending. No header means nothing was held.' );
998 +
999 + if ( 'cloudflare' === $answer['provider'] ) {
1000 + \WP_CLI::log( '' );
1001 + \WP_CLI::log( 'A Cloudflare Cache Rule whose Edge TTL is "Ignore cache-control header and use this TTL" overrides all of the above. Use "Respect origin TTL" on that rule if pages are still being stored.' );
841 1002 }
842 1003 }
843 1004
844 1005 /** `wp xspeed cache inventory [--limit=N]` — which pages are cached, and how old. */