| @@ -107,9 +107,48 @@ | ||
| 107 | 107 | 'usqp', |
| 108 | 108 | '~utm_[a-zA-Z0-9_-]+', |
| 109 | 109 | ); |
| 110 | 110 | |
| 111 | + /** | |
| 112 | + * Click and campaign IDs added to the defaults in 1.3.6. | |
| 113 | + * | |
| 114 | + * Each is unique per click and never changes the page, yet each one | |
| 115 | + * bypassed the page cache and, with xSpeed Pro, was a new CSS entry to | |
| 116 | + * build: Google Merchant's `srsltid` and Ads' `gad_*`/`gbraid`/`wbraid`, | |
| 117 | + * GA4's cross-domain `_gl`, TikTok, X, Instagram, Yandex, HubSpot email | |
| 118 | + * and LinkedIn IDs. Kept as their own list so the upgrade can add exactly | |
| 119 | + * these to a saved list without re-adding anything a site removed. | |
| 120 | + * | |
| 121 | + * @var string[] | |
| 122 | + */ | |
| 123 | + public const TRACKING_PARAMS_1_3_6 = array( | |
| 124 | + '_gl', | |
| 125 | + '_hsenc', | |
| 126 | + '_hsmi', | |
| 127 | + 'gad_campaignid', | |
| 128 | + 'gad_source', | |
| 129 | + 'gbraid', | |
| 130 | + 'igshid', | |
| 131 | + 'li_fat_id', | |
| 132 | + 'srsltid', | |
| 133 | + 'ttclid', | |
| 134 | + 'twclid', | |
| 135 | + 'wbraid', | |
| 136 | + 'yclid', | |
| 137 | + ); | |
| 111 | 138 | |
| 139 | + | |
| 140 | + /** | |
| 141 | + * The shipped default list: the base list plus later additions, sorted. | |
| 142 | + * | |
| 143 | + * @return string[] | |
| 144 | + */ | |
| 145 | + public static function default_ignored_query_params(): array { | |
| 146 | + $all = array_values( array_unique( array_merge( self::DEFAULT_IGNORED_QUERY_PARAMS, self::TRACKING_PARAMS_1_3_6 ) ) ); | |
| 147 | + sort( $all ); | |
| 148 | + return $all; | |
| 149 | + } | |
| 150 | + | |
| 112 | 151 | public const SLUG = 'cache'; |
| 113 | 152 | public const TIER = self::TIER_FREE; |
| 114 | 153 | public const VERSION = '1.0.0'; |
| 115 | 154 | |
| @@ -128,9 +167,10 @@ | ||
| 128 | 167 | public function ui_metadata(): array { |
| 129 | 168 | return array( |
| 130 | 169 | 'label' => __( 'Page Cache', 'xspeed' ), |
| 131 | 170 | 'icon' => 'Database', |
| 132 | - 'description' => __( 'Page caching for non-logged-in visitors.', 'xspeed' ), | |
| 171 | + 'description' => __( 'Saves each page as a file and serves it to logged-out visitors.', 'xspeed' ), | |
| 172 | + 'group' => 'cache', | |
| 133 | 173 | ); |
| 134 | 174 | } |
| 135 | 175 | |
| 136 | 176 | /** |
| @@ -145,9 +185,9 @@ | ||
| 145 | 185 | return array(); |
| 146 | 186 | } |
| 147 | 187 | |
| 148 | 188 | public function settings_schema(): array { |
| 149 | - return array( | |
| 189 | + $schema = array( | |
| 150 | 190 | 'cache_expiry' => array( |
| 151 | 191 | 'type' => 'int', |
| 152 | 192 | // Matches the wizard's Balanced preset, which is what a fresh |
| 153 | 193 | // install starts on — a shorter module default meant the two |
| @@ -154,11 +194,11 @@ | ||
| 154 | 194 | // disagreed about what "default" means. (#284) |
| 155 | 195 | 'default' => self::DEFAULT_EXPIRY_HOURS, |
| 156 | 196 | 'min' => 1, |
| 157 | 197 | 'max' => 720, |
| 158 | - 'label' => __( 'Cache Expiry (hours)', 'xspeed' ), | |
| 198 | + 'label' => __( 'Cache expiry (hours)', 'xspeed' ), | |
| 159 | 199 | 'unit' => 'hours', |
| 160 | - 'description' => __( 'How long cached pages live before regenerating. 1 to 720 hours (30 days).', 'xspeed' ), | |
| 200 | + 'description' => __( 'How long a cached page is kept before xSpeed builds it again. 1 to 720 hours (30 days).', 'xspeed' ), | |
| 161 | 201 | ), |
| 162 | 202 | 'excluded_urls' => array( |
| 163 | 203 | 'type' => 'list', |
| 164 | 204 | // Comprehensive LiteSpeed / WP Rocket-parity default URL |
| @@ -185,11 +225,12 @@ | ||
| 185 | 225 | '/wc-api', |
| 186 | 226 | '/edd-api', |
| 187 | 227 | '/wp-login', |
| 188 | 228 | ), |
| 189 | - 'item_type' => 'string', | |
| 229 | + // `path`: kept as typed, percent-encoded slugs included. | |
| 230 | + 'item_type' => 'path', | |
| 190 | 231 | 'label' => __( 'Excluded URLs', 'xspeed' ), |
| 191 | - 'description' => __( 'One pattern per line. Plain text matches anywhere in the URL (e.g. /cart). Use glob for anchored matches (/cart/* matches /cart/items but not /foo/cart/bar; *.pdf matches PDFs). Prefix with ~ for a raw regex (e.g. ~wp-.*\.php).', 'xspeed' ), | |
| 232 | + 'description' => __( 'Pages whose URL matches a line here are never cached. Plain text matches anywhere (/cart). A pattern with * matches from the start of the path: /cart/* matches /cart/items but not /shop/cart/items. ~ starts a regex.', 'xspeed' ), | |
| 192 | 233 | ), |
| 193 | 234 | 'excluded_cookies' => array( |
| 194 | 235 | 'type' => 'list', |
| 195 | 236 | // Cookies that signal a logged-in / transactional visitor |
| @@ -196,17 +237,17 @@ | ||
| 196 | 237 | // whose response must not be served from a shared cache. |
| 197 | 238 | // `~` prefix = raw regex (e.g. ~wordpress_[a-f0-9]+). (FBS-82181) |
| 198 | 239 | 'default' => self::DEFAULT_EXCLUDED_COOKIES, |
| 199 | 240 | 'item_type' => 'string', |
| 200 | - 'label' => __( 'Excluded Cookies', 'xspeed' ), | |
| 201 | - 'description' => __( 'Skip cache for any visitor whose request carries a cookie whose NAME matches one of these patterns. Plain text = "contains"; glob (woocommerce_*) and ~regex (~wordpress_[a-f0-9]+) supported. One per line.', 'xspeed' ), | |
| 241 | + 'label' => __( 'Excluded cookies', 'xspeed' ), | |
| 242 | + 'description' => __( 'Visitors with a cookie whose name matches a line here always get a fresh page. One per line; * and ~regex work.', 'xspeed' ), | |
| 202 | 243 | ), |
| 203 | 244 | 'bypass_user_agents' => array( |
| 204 | 245 | 'type' => 'list', |
| 205 | 246 | 'default' => array(), |
| 206 | 247 | 'item_type' => 'string', |
| 207 | - 'label' => __( 'Bypass User Agents', 'xspeed' ), | |
| 208 | - 'description' => __( 'Substring match against the visitor User-Agent. Matched UAs bypass cache (useful for screenshot bots, internal previews, monitoring). Glob + ~regex supported. One per line.', 'xspeed' ), | |
| 248 | + 'label' => __( 'Excluded browsers and bots', 'xspeed' ), | |
| 249 | + 'description' => __( 'Visitors whose user agent contains a line here always get a fresh page. Useful for screenshot bots and uptime monitors.', 'xspeed' ), | |
| 209 | 250 | ), |
| 210 | 251 | 'ignored_query_params' => array( |
| 211 | 252 | 'type' => 'list', |
| 212 | 253 | // Analytics / ad / session query keys stripped before the |
| @@ -213,24 +254,33 @@ | ||
| 213 | 254 | // cache key is computed, so /post?utm_source=x and /post |
| 214 | 255 | // share one entry. `~` prefix = raw regex. (FBS-82181) |
| 215 | 256 | // Matched whole-name, so every entry here means the param |
| 216 | 257 | // it names and nothing that merely contains it. |
| 217 | - 'default' => self::DEFAULT_IGNORED_QUERY_PARAMS, | |
| 258 | + 'default' => self::default_ignored_query_params(), | |
| 218 | 259 | 'item_type' => 'string', |
| 219 | - 'label' => __( 'Ignored Query Parameters', 'xspeed' ), | |
| 220 | - 'description' => __( 'Query keys removed from the URL before computing the cache key, so /post?utm_source=x and /post share a cache entry. Defaults cover the common analytics + ad + session params. Each entry matches a whole param name — plain text is an exact name, and glob (utm_*) or ~regex are anchored too, so "ref" does not also match "preference". One per line.', 'xspeed' ), | |
| 260 | + 'label' => __( 'Ignored query parameters', 'xspeed' ), | |
| 261 | + 'advanced' => true, | |
| 262 | + 'description' => __( 'URL parameters to ignore, so /post?utm_source=x gets the same cached page as /post. Each line matches a whole parameter name.', 'xspeed' ), | |
| 221 | 263 | ), |
| 222 | 264 | 'purge_on_upgrade' => array( |
| 223 | 265 | 'type' => 'bool', |
| 224 | 266 | 'default' => true, |
| 225 | - 'label' => __( 'Purge After Updates', 'xspeed' ), | |
| 226 | - 'description' => __( 'Clear the page cache when a plugin, theme or WordPress core is updated. Cached HTML is produced by the code being replaced, so leaving it in place serves pre-update markup — and links to minified assets that no longer exist — until the cache expires. Translation updates are ignored, since a language pack changes no markup a cached page depends on. Updates to xSpeed itself always purge, regardless of this setting.', 'xspeed' ), | |
| 267 | + 'label' => __( 'Clear cache after updates', 'xspeed' ), | |
| 268 | + 'description' => __( 'Clear the page cache when a plugin, theme or WordPress is updated, so visitors never get old pages with broken asset links.', 'xspeed' ), | |
| 227 | 269 | ), |
| 270 | + 'purge_affected_only' => array( | |
| 271 | + 'type' => 'bool', | |
| 272 | + 'default' => true, | |
| 273 | + 'label' => __( 'Clear only the pages a change affects', 'xspeed' ), | |
| 274 | + 'description' => __( 'When you publish, edit or delete a post, clear that post, the pages that list it and its feeds instead of the whole site. Pages with a post grid from a page builder, a block plugin or the theme are noted as they are built and cleared when the change can affect their list; turn this off if a grid still shows an old list.', 'xspeed' ), | |
| 275 | + 'info_title' => __( 'Which pages are cleared', 'xspeed' ), | |
| 276 | + 'info' => __( 'The post, its old address if it moved, the home page, the blog page, its categories, tags and author pages with every page of each, the date archives, feeds, the posts next to it, pages that show a Latest Posts or Query Loop block, directly or in a synced pattern, and pages noted running a post grid of their own (a page builder, block plugin, related-posts or theme list) that the change can affect. If your theme shows a list that can be on any page, and the change alters it, xSpeed clears the whole site instead: a Recent Posts, Archives or Calendar widget, a post list block, or a latest-posts grid from a page builder or plugin (usually a new post, or an edit to one of the newest few it shows), a page list or menu (a page published, withdrawn, renamed or moved), or a Categories or Tag Cloud list (a post moved to other categories or tags).', 'xspeed' ), | |
| 277 | + ), | |
| 228 | 278 | 'mobile_separate' => array( |
| 229 | 279 | 'type' => 'bool', |
| 230 | 280 | 'default' => false, |
| 231 | - 'label' => __( 'Separate Mobile Cache', 'xspeed' ), | |
| 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' ), | |
| 281 | + 'label' => __( 'Separate mobile cache', 'xspeed' ), | |
| 282 | + 'description' => __( 'Keep a separate cached copy for phones. Turn on only if your theme or plugins show different pages on mobile.', 'xspeed' ), | |
| 233 | 283 | ), |
| 234 | 284 | 'edge_provider' => array( |
| 235 | 285 | 'type' => 'enum', |
| 236 | 286 | 'default' => 'auto', |
| @@ -236,9 +286,9 @@ | ||
| 236 | 286 | 'default' => 'auto', |
| 237 | 287 | 'options' => array( 'auto', 'off', 'cloudflare', 'fastly', 'varnish', 'nginx', 'akamai', 'cloudfront', 'google', 'keycdn', 'bunny', 'sucuri', 'incapsula', 'generic', 'custom' ), |
| 238 | 288 | 'option_labels' => array( |
| 239 | 289 | 'auto' => 'Detect automatically', |
| 240 | - 'off' => 'Off — send nothing', | |
| 290 | + 'off' => 'Off (no edge lifetime or cache tags)', | |
| 241 | 291 | 'cloudflare' => 'Cloudflare', |
| 242 | 292 | 'varnish' => 'Varnish', |
| 243 | 293 | 'nginx' => 'nginx proxy cache', |
| 244 | 294 | 'cloudfront' => 'Amazon CloudFront', |
| @@ -260,24 +310,46 @@ | ||
| 260 | 310 | 'incapsula' => 'Imperva / Incapsula (needs CDN configuration)', |
| 261 | 311 | 'generic' => 'Something else', |
| 262 | 312 | 'custom' => 'Custom headers', |
| 263 | 313 | ), |
| 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' ), | |
| 314 | + 'label' => __( 'CDN or proxy in front', 'xspeed' ), | |
| 315 | + 'description' => __( 'Tells your CDN or proxy not to store pages xSpeed does not cache. Leave on Detect automatically unless you know your provider.', 'xspeed' ), | |
| 316 | + 'advanced' => true, | |
| 266 | 317 | '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' ) | |
| 318 | + '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. Off stops the edge lifetime and cache tags. When an add-on that manages the edge, such as a Cloudflare Enterprise add-on, is active, the "don\'t store" headers for logged-in, cart and search pages still go out. Run "wp xspeed cache edge" to see what was detected and what gets sent.', 'xspeed' ) | |
| 268 | 319 | ), |
| 269 | 320 | 'edge_custom_headers' => array( |
| 270 | 321 | 'type' => 'list', |
| 271 | 322 | 'default' => array(), |
| 272 | 323 | 'item_type' => 'string', |
| 273 | - 'label' => __( 'Custom Edge Headers', 'xspeed' ), | |
| 324 | + 'label' => __( 'Custom CDN headers', 'xspeed' ), | |
| 325 | + 'advanced' => true, | |
| 274 | 326 | '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' ), | |
| 327 | + 'description' => __( 'One header per line, as Name: value, for example "Surrogate-Control: no-store". Lines starting with # are ignored.', 'xspeed' ), | |
| 276 | 328 | 'info_title' => __( 'Custom edge headers', 'xspeed' ), |
| 277 | 329 | '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 | 330 | ), |
| 279 | 331 | ); |
| 332 | + | |
| 333 | + // LiteSpeed-only opt-in (#509): meaningless on any other server, so | |
| 334 | + // the field only exists in the schema where it can act — elsewhere the | |
| 335 | + // stored value survives via preserved_keys(). Inserted right after | |
| 336 | + // mobile_separate, its sibling static-fast-path trade-off. | |
| 337 | + if ( \XSpeed\Server::LITESPEED === \XSpeed\Server::type() ) { | |
| 338 | + $litespeed = array( | |
| 339 | + 'litespeed_static_rewrite' => array( | |
| 340 | + 'type' => 'bool', | |
| 341 | + 'default' => false, | |
| 342 | + 'label' => __( 'LiteSpeed static fast path', 'xspeed' ), | |
| 343 | + 'advanced' => true, | |
| 344 | + 'description' => __( 'LiteSpeed serves cached pages without running PHP, which is faster on slow hosts. These visits are not counted in the dashboard hit ratio.', 'xspeed' ), | |
| 345 | + ), | |
| 346 | + ); | |
| 347 | + $pos = (int) array_search( 'mobile_separate', array_keys( $schema ), true ) + 1; | |
| 348 | + $schema = array_slice( $schema, 0, $pos, true ) + $litespeed + array_slice( $schema, $pos, null, true ); | |
| 349 | + } | |
| 350 | + | |
| 351 | + return $schema; | |
| 280 | 352 | } |
| 281 | 353 | |
| 282 | 354 | /** |
| 283 | 355 | * `mobile_separate_review` lives outside the schema: migration sets it |
| @@ -290,9 +362,18 @@ | ||
| 290 | 362 | * |
| 291 | 363 | * @return string[] |
| 292 | 364 | */ |
| 293 | 365 | public function preserved_keys(): array { |
| 294 | - return array( 'mobile_separate_review' ); | |
| 366 | + $keys = array( 'mobile_separate_review' ); | |
| 367 | + // On non-LiteSpeed servers the litespeed_static_rewrite field is not | |
| 368 | + // in the schema (see settings_schema()), so a schema-driven save | |
| 369 | + // would silently drop a value chosen while the site ran LiteSpeed. | |
| 370 | + // Preserve it so moving LiteSpeed → other → LiteSpeed keeps the | |
| 371 | + // user's choice. On LiteSpeed itself the schema owns the key. | |
| 372 | + if ( \XSpeed\Server::LITESPEED !== \XSpeed\Server::type() ) { | |
| 373 | + $keys[] = 'litespeed_static_rewrite'; | |
| 374 | + } | |
| 375 | + return $keys; | |
| 295 | 376 | } |
| 296 | 377 | |
| 297 | 378 | /** |
| 298 | 379 | * Seed per-module option from the legacy xspeed_options blob if we |
| @@ -442,9 +523,9 @@ | ||
| 442 | 523 | 'synopsis' => array( |
| 443 | 524 | array( |
| 444 | 525 | 'type' => 'assoc', |
| 445 | 526 | 'name' => 'type', |
| 446 | - 'description' => 'What to clear: all (default), page, object, cloudflare, cdn — or a group name (edge). Comma-separate to clear several.', | |
| 527 | + 'description' => 'What to clear: all (default), page, object, cloudflare, xcloud (sites with xCloud\'s purge plugin), cdn — or a group name (edge). Comma-separate to clear several.', | |
| 447 | 528 | 'optional' => true, |
| 448 | 529 | ), |
| 449 | 530 | array( |
| 450 | 531 | 'type' => 'assoc', |
| @@ -463,14 +544,14 @@ | ||
| 463 | 544 | ), |
| 464 | 545 | array( |
| 465 | 546 | 'name' => 'xspeed cache', |
| 466 | 547 | 'callback' => array( $this, 'cli_handler' ), |
| 467 | - '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`.', | |
| 548 | + '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, or `listings` to show which pages were recorded running a post list of their own (cleared by a narrow purge when a save may change that list). To clear the whole site use `wp xspeed purge`.', | |
| 468 | 549 | 'synopsis' => array( |
| 469 | 550 | array( |
| 470 | 551 | 'type' => 'positional', |
| 471 | 552 | 'name' => 'action', |
| 472 | - 'options' => array( 'status', 'inventory', 'size', 'purge-log', 'purge-url', 'recheck-rewrite', 'nginx-config', 'edge' ), | |
| 553 | + 'options' => array( 'status', 'inventory', 'size', 'purge-log', 'purge-url', 'recheck-rewrite', 'nginx-config', 'edge', 'listings' ), | |
| 473 | 554 | 'optional' => true, |
| 474 | 555 | ), |
| 475 | 556 | array( |
| 476 | 557 | 'type' => 'positional', |
| @@ -479,9 +560,9 @@ | ||
| 479 | 560 | ), |
| 480 | 561 | array( |
| 481 | 562 | 'type' => 'assoc', |
| 482 | 563 | 'name' => 'limit', |
| 483 | - 'description' => 'Rows to print for inventory / purge-log. Default 20.', | |
| 564 | + 'description' => 'Rows to print for inventory / purge-log (default 20), or sample pages for listings (default 5).', | |
| 484 | 565 | 'optional' => true, |
| 485 | 566 | ), |
| 486 | 567 | array( |
| 487 | 568 | 'type' => 'assoc', |
| @@ -831,8 +912,15 @@ | ||
| 831 | 912 | // HIT (php). See Cache::qualify_rewrite_probe(). |
| 832 | 913 | $probe = \XSpeed\Cache::qualify_rewrite_probe( \XSpeed\Cache::recheck_static_rewrite() ); |
| 833 | 914 | $blocked = '' !== (string) $probe['block_reason']; |
| 834 | 915 | |
| 916 | + // Whether the installed rules are the ones these settings | |
| 917 | + // generate. Reported before the verdict below because it is the | |
| 918 | + // question someone running this command has just acted on — they | |
| 919 | + // pasted a block and want to know if it took — and because | |
| 920 | + // "active" is true for a stale block too. | |
| 921 | + $this->cli_report_rules_state( (array) ( $probe['rules'] ?? array() ) ); | |
| 922 | + | |
| 835 | 923 | if ( $probe['active'] ) { |
| 836 | 924 | \WP_CLI::success( 'Static rewrite is active — the web server is serving cache hits directly.' ); |
| 837 | 925 | return; |
| 838 | 926 | } |
| @@ -855,11 +943,21 @@ | ||
| 855 | 943 | \WP_CLI::error( 'Usage: wp xspeed cache purge-url <url-or-path>' ); |
| 856 | 944 | return; |
| 857 | 945 | } |
| 858 | 946 | $cause = isset( $assoc['cause'] ) && '' !== trim( (string) $assoc['cause'] ) ? trim( (string) $assoc['cause'] ) : 'CLI'; |
| 859 | - $removed = \XSpeed\Cache::purge_url( $url, $cause ); | |
| 947 | + $result = \XSpeed\Cache::purge_url_reported( $url, $cause ); | |
| 948 | + $removed = $result['removed']; | |
| 949 | + $forwarded = implode( ', ', $result['forwarded'] ); | |
| 860 | 950 | if ( $removed > 0 ) { |
| 861 | - \WP_CLI::success( sprintf( 'Purged %d cache file(s) for %s', $removed, $url ) ); | |
| 951 | + \WP_CLI::success( | |
| 952 | + '' === $forwarded | |
| 953 | + ? sprintf( 'Purged %d cache file(s) for %s', $removed, $url ) | |
| 954 | + : sprintf( 'Purged %d cache file(s) for %s, and sent the purge to %s', $removed, $url, $forwarded ) | |
| 955 | + ); | |
| 956 | + } elseif ( '' !== $forwarded ) { | |
| 957 | + // xSpeed's own cache held nothing, but a cache in front of | |
| 958 | + // PHP took the purge: that copy is the one visitors get. | |
| 959 | + \WP_CLI::success( sprintf( 'Sent the purge for %s to %s. xSpeed\'s own cache held no copy.', $url, $forwarded ) ); | |
| 862 | 960 | } else { |
| 863 | 961 | \WP_CLI::log( sprintf( 'No cache entries found for %s (already cold, or the URL never cached).', $url ) ); |
| 864 | 962 | } |
| 865 | 963 | return; |
| @@ -884,8 +982,13 @@ | ||
| 884 | 982 | $this->cli_edge(); |
| 885 | 983 | return; |
| 886 | 984 | } |
| 887 | 985 | |
| 986 | + if ( 'listings' === $action ) { | |
| 987 | + $this->cli_listings( isset( $assoc['limit'] ) ? $limit : 5 ); | |
| 988 | + return; | |
| 989 | + } | |
| 990 | + | |
| 888 | 991 | $opts = Settings_Manager::get( self::SLUG ); |
| 889 | 992 | \WP_CLI::log( 'cache_expiry ' . $opts['cache_expiry'] . 'h' ); |
| 890 | 993 | \WP_CLI::log( 'excluded_urls ' . count( $opts['excluded_urls'] ) . ' entries' ); |
| 891 | 994 | foreach ( $opts['excluded_urls'] as $u ) { |
| @@ -964,15 +1067,131 @@ | ||
| 964 | 1067 | \WP_CLI::log( sprintf( ' %s: %s', $name, $value ) ); |
| 965 | 1068 | } |
| 966 | 1069 | } |
| 967 | 1070 | |
| 1071 | + // The same bake the drop-in gets, so it asks no per-page question. | |
| 1072 | + $variant = \XSpeed\Cache::query_variant_edge_headers(); | |
| 1073 | + | |
| 968 | 1074 | \WP_CLI::log( '' ); |
| 969 | - \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.' ); | |
| 1075 | + if ( array() === $variant ) { | |
| 1076 | + \WP_CLI::log( 'A cached page requested with an ignored parameter (?utm_source=x) gets the same headers as without one. It is held only where a cache in front was detected, and with Separate Mobile Cache on every page is held already.' ); | |
| 1077 | + } else { | |
| 1078 | + \WP_CLI::log( 'On a cached page requested with an ignored parameter (?utm_source=x), a cached search or a query-form feed:' ); | |
| 1079 | + foreach ( $variant as $name => $value ) { | |
| 1080 | + \WP_CLI::log( sprintf( ' %s: %s', $name, $value ) ); | |
| 1081 | + } | |
| 1082 | + } | |
| 970 | 1083 | |
| 1084 | + \WP_CLI::log( '' ); | |
| 1085 | + \WP_CLI::log( 'X-XSpeed-Edge-Hold names why a response was held: bypass, bypass-shape, miss, mobile-split, pending or query-variant. No header means nothing was held.' ); | |
| 1086 | + | |
| 971 | 1087 | if ( 'cloudflare' === $answer['provider'] ) { |
| 972 | 1088 | \WP_CLI::log( '' ); |
| 973 | 1089 | \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.' ); |
| 974 | 1090 | } |
| 1091 | + } | |
| 1092 | + | |
| 1093 | + /** | |
| 1094 | + * `wp xspeed cache listings`: the pages recorded running a post list | |
| 1095 | + * outside the main loop, which a narrow purge adds when a save may | |
| 1096 | + * change that list. | |
| 1097 | + * | |
| 1098 | + * For measuring how wide that set gets on a real site: close to every | |
| 1099 | + * page means a narrow purge saves little there. | |
| 1100 | + * | |
| 1101 | + * @param int $samples Pages to print. | |
| 1102 | + */ | |
| 1103 | + private function cli_listings( int $samples ): void { | |
| 1104 | + $narrow = ! empty( Settings_Manager::get( self::SLUG )['purge_affected_only'] ); | |
| 1105 | + $stats = \XSpeed\Listing_Pages::stats( $samples ); | |
| 1106 | + $limit = \XSpeed\Affected_Pages::LIMIT; | |
| 1107 | + | |
| 1108 | + \WP_CLI::log( 'narrow purge ' . ( $narrow ? 'on' : 'off' ) ); | |
| 1109 | + \WP_CLI::log( 'recording ' . ( \XSpeed\Listing_Pages::enabled() ? 'on' : 'off' ) ); | |
| 1110 | + \WP_CLI::log( 'directory ' . $stats['dir'] ); | |
| 1111 | + | |
| 1112 | + \WP_CLI::log( '' ); | |
| 1113 | + \WP_CLI::log( 'Newest-N lists (' . count( $stats['specs'] ) . ' of ' . \XSpeed\Listing_Pages::SPEC_CAP . '): a change to one of the newest N clears the whole site.' ); | |
| 1114 | + foreach ( $stats['specs'] as $spec ) { | |
| 1115 | + \WP_CLI::log( sprintf( ' newest %d of %s%s', $spec['n'], implode( ', ', $spec['types'] ), $spec['sticky'] ? ', sticky posts first' : '' ) ); | |
| 1116 | + } | |
| 1117 | + | |
| 1118 | + \WP_CLI::log( '' ); | |
| 1119 | + \WP_CLI::log( sprintf( 'Pages with other lists: %d recorded (%d files, cap %d); %d posts in the shown-on index.', $stats['pages'], $stats['files'], \XSpeed\Listing_Pages::CAP, $stats['posts'] ) ); | |
| 1120 | + foreach ( $stats['types'] as $type => $counts ) { | |
| 1121 | + \WP_CLI::log( | |
| 1122 | + sprintf( | |
| 1123 | + ' %-14s %d pages, %d not precise%s', | |
| 1124 | + $type, | |
| 1125 | + $counts['all'], | |
| 1126 | + $counts['loose'], | |
| 1127 | + $counts['loose'] > $limit | |
| 1128 | + ? ' (over ' . $limit . ' not precise: any change to a post of this type clears the whole site)' | |
| 1129 | + : ( $counts['all'] > $limit ? ' (over ' . $limit . ': a change that is not a plain edit clears the whole site)' : '' ) | |
| 1130 | + ) | |
| 1131 | + ); | |
| 1132 | + } | |
| 1133 | + | |
| 1134 | + if ( null !== $stats['full'] ) { | |
| 1135 | + \WP_CLI::warning( | |
| 1136 | + sprintf( | |
| 1137 | + 'The record is full, and pages listing %s went unrecorded: a change to a post of those types clears the whole site until a save finds the record under the cap.', | |
| 1138 | + implode( ', ', array_map( 'strval', (array) ( $stats['full']['types'] ?? array( 'any' ) ) ) ) | |
| 1139 | + ) | |
| 1140 | + ); | |
| 1141 | + } | |
| 1142 | + if ( array() === $stats['samples'] ) { | |
| 1143 | + return; | |
| 1144 | + } | |
| 1145 | + \WP_CLI::log( '' ); | |
| 1146 | + foreach ( $stats['samples'] as $record ) { | |
| 1147 | + \WP_CLI::log( | |
| 1148 | + sprintf( | |
| 1149 | + ' %s [%s] %s', | |
| 1150 | + $record['url'], | |
| 1151 | + implode( ', ', $record['types'] ), | |
| 1152 | + $record['precise'] ? 'precise, ' . count( $record['ids'] ) . ' post(s) shown' : 'not precise' | |
| 1153 | + ) | |
| 1154 | + ); | |
| 1155 | + } | |
| 1156 | + } | |
| 1157 | + | |
| 1158 | + /** | |
| 1159 | + * Say which version of the cache rules the server is running. | |
| 1160 | + * | |
| 1161 | + * The generated block stamps every static hit with a hash of itself, and | |
| 1162 | + * the probe reads that back — the only way to tell what someone actually | |
| 1163 | + * pasted into a config WordPress cannot open. `stale` names the settings | |
| 1164 | + * that moved since, where we can tell, because "re-paste the block" with | |
| 1165 | + * no reason attached is what makes people ignore it. | |
| 1166 | + * | |
| 1167 | + * @param array $rules Cache::rules_state() output. | |
| 1168 | + */ | |
| 1169 | + private function cli_report_rules_state( array $rules ): void { | |
| 1170 | + $state = (string) ( $rules['state'] ?? '' ); | |
| 1171 | + | |
| 1172 | + if ( 'current' === $state ) { | |
| 1173 | + \WP_CLI::log( 'Cache rules: current — the installed rules are the ones these settings generate.' ); | |
| 1174 | + return; | |
| 1175 | + } | |
| 1176 | + if ( 'stale' === $state ) { | |
| 1177 | + $changed = array_filter( array_map( 'strval', (array) ( $rules['changed'] ?? array() ) ) ); | |
| 1178 | + \WP_CLI::log( | |
| 1179 | + '' === implode( '', $changed ) | |
| 1180 | + ? 'Cache rules: stale — a different version of the rules is installed. Re-paste the block.' | |
| 1181 | + : sprintf( | |
| 1182 | + 'Cache rules: stale — a different version of the rules is installed (%s changed since). Re-paste the block.', | |
| 1183 | + implode( ', ', $changed ) | |
| 1184 | + ) | |
| 1185 | + ); | |
| 1186 | + return; | |
| 1187 | + } | |
| 1188 | + if ( 'absent' === $state ) { | |
| 1189 | + \WP_CLI::log( 'Cache rules: absent — no xSpeed rules are installed, or they predate the version marker.' ); | |
| 1190 | + return; | |
| 1191 | + } | |
| 1192 | + | |
| 1193 | + \WP_CLI::log( 'Cache rules: unknown — the probe could not read a rules marker back from this server.' ); | |
| 975 | 1194 | } |
| 976 | 1195 | |
| 977 | 1196 | /** `wp xspeed cache inventory [--limit=N]` — which pages are cached, and how old. */ |
| 978 | 1197 | private function cli_inventory( int $limit ): void { |