| @@ -4,10 +4,16 @@ | ||
| 4 | 4 | * |
| 5 | 5 | * xSpeed owns one cache. A LiteSpeed stack has two: ours, and LSCache holding |
| 6 | 6 | * its own copy of the same URL at the server. Purging ours and stopping there |
| 7 | 7 | * left the server still serving the page we had just invalidated — measured on |
| 8 | - * OpenLiteSpeed before this existed. | |
| 8 | + * OpenLiteSpeed before this existed. Two adapters ship: LiteSpeed, and the | |
| 9 | + * nginx FastCGI cache reached through the Nginx Helper plugin. | |
| 9 | 10 | * |
| 11 | + * Each adapter decides for itself which purges are worth forwarding, from the | |
| 12 | + * `intent` and `scope` on the context. They do not answer alike, and the | |
| 13 | + * reasoning for each lives on the adapter — see `forward_nginx_helper()`, | |
| 14 | + * which stands down on a content purge where `forward_litespeed()` does not. | |
| 15 | + * | |
| 10 | 16 | * This is the counterpart to Render_Caches. That one clears caches of RENDERED |
| 11 | 17 | * OUTPUT owned by page builders; this one clears caches of whole RESPONSES |
| 12 | 18 | * owned by the web server. Both are integrations with software we do not ship, |
| 13 | 19 | * and both hang off a public seam so a site can add its own. |
| @@ -33,10 +39,10 @@ | ||
| 33 | 39 | |
| 34 | 40 | final class Server_Caches { |
| 35 | 41 | |
| 36 | 42 | /* |
| 37 | - * There is deliberately no boot()/add_action here. `Cache` calls forward() | |
| 38 | - * directly, before it fires the public purge actions. | |
| 43 | + * Forwarding itself is deliberately not a listener. `Cache` calls | |
| 44 | + * forward() directly, before it fires the public purge actions. | |
| 39 | 45 | * |
| 40 | 46 | * As a listener this would be one callback among many, and WordPress stops |
| 41 | 47 | * dispatching an action's remaining callbacks when an earlier one throws — |
| 42 | 48 | * so an unrelated third-party listener's bug could silently skip our |
| @@ -43,11 +49,59 @@ | ||
| 43 | 49 | * LiteSpeed forwarding, leaving the server serving stale HTML while xSpeed |
| 44 | 50 | * reported a successful purge. Shipped behaviour should not be hostage to |
| 45 | 51 | * that. Third parties still extend through `xspeed_purge_server_caches` |
| 46 | 52 | * below, which runs after we have done our own work. |
| 53 | + * | |
| 54 | + * Both built-in adapters are reached only from forward(). Neither | |
| 55 | + * registers a hook of its own, so this is the single place that decides | |
| 56 | + * whether a given purge reaches a server cache. | |
| 57 | + * | |
| 58 | + * boot() below is the one exception, and it registers nothing that | |
| 59 | + * forwards — only the end-of-import purge that forward_nginx_helper()'s | |
| 60 | + * import gate depends on. | |
| 47 | 61 | */ |
| 48 | 62 | |
| 49 | 63 | /** |
| 64 | + * Register the end-of-import purge. | |
| 65 | + * | |
| 66 | + * `forward_nginx_helper()` stands down for the length of an import: a | |
| 67 | + * WXR run fires hundreds of individually-justified purges, and clearing | |
| 68 | + * the whole nginx zone once per imported post is the waste that gate | |
| 69 | + * exists to stop. That trade is only correct if a single purge follows | |
| 70 | + * the import — otherwise the install finishes with nginx still serving | |
| 71 | + * every pre-import page for the rest of its TTL, which is worse than the | |
| 72 | + * waste. This is that purge, and nothing else issues it. | |
| 73 | + * | |
| 74 | + * `import_end` is WordPress's own signal, fired by the WXR importer and | |
| 75 | + * by every importer that follows its lead. An importer that fires | |
| 76 | + * `import_start` and then dies without `import_end` leaves the zone | |
| 77 | + * stale — the same outcome as not having the gate, so no worse than | |
| 78 | + * before, and not worth a `shutdown` fallback that would fire a full | |
| 79 | + * purge on every request that ever touched an importer. | |
| 80 | + */ | |
| 81 | + public static function boot(): void { | |
| 82 | + if ( ! function_exists( 'add_action' ) ) { | |
| 83 | + return; | |
| 84 | + } | |
| 85 | + add_action( 'import_end', array( __CLASS__, 'purge_after_import' ) ); | |
| 86 | + } | |
| 87 | + | |
| 88 | + /** | |
| 89 | + * Clear everything once, now that the import is done. | |
| 90 | + * | |
| 91 | + * `complete` intent, which is what `Cache::purge_all()` announces by | |
| 92 | + * default — and the one intent the import gate lets through, so this | |
| 93 | + * reaches the server layer even though `did_action( 'import_start' )` is | |
| 94 | + * still true for the rest of the request. | |
| 95 | + */ | |
| 96 | + public static function purge_after_import(): void { | |
| 97 | + if ( ! class_exists( __NAMESPACE__ . '\\Cache' ) ) { | |
| 98 | + return; | |
| 99 | + } | |
| 100 | + Cache::purge_all( 'import finished' ); | |
| 101 | + } | |
| 102 | + | |
| 103 | + /** | |
| 50 | 104 | * Forward one purge to every server cache we recognise. |
| 51 | 105 | * |
| 52 | 106 | * The public context carries the action an adapter should take: |
| 53 | 107 | * `urls` purges only the listed response URLs, `site` purges this site's |
| @@ -71,8 +125,20 @@ | ||
| 71 | 125 | error_log( '[xspeed] LiteSpeed response purge failed: ' . $e->getMessage() ); |
| 72 | 126 | } |
| 73 | 127 | } |
| 74 | 128 | |
| 129 | + // Its own try, for the same reason the LiteSpeed one has its own: two | |
| 130 | + // server caches can be in front of one site, and a bad day for one | |
| 131 | + // adapter must not leave the other serving stale HTML. | |
| 132 | + try { | |
| 133 | + self::forward_nginx_helper( $context ); | |
| 134 | + } catch ( \Throwable $e ) { | |
| 135 | + if ( defined( 'WP_DEBUG' ) && WP_DEBUG && function_exists( 'error_log' ) ) { | |
| 136 | + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- built-in integration failure after local invalidation. | |
| 137 | + error_log( '[xspeed] nginx FastCGI response purge failed: ' . $e->getMessage() ); | |
| 138 | + } | |
| 139 | + } | |
| 140 | + | |
| 75 | 141 | /** |
| 76 | 142 | * Fires so a site can invalidate a server cache xSpeed does not know. |
| 77 | 143 | * |
| 78 | 144 | * Same context as the event that triggered it. Use this rather than |
| @@ -104,8 +170,12 @@ | ||
| 104 | 170 | * |
| 105 | 171 | * Detected by constant, not plugin path. `LSCWP_V` is defined by the |
| 106 | 172 | * plugin bootstrap and survives a renamed folder. |
| 107 | 173 | * |
| 174 | + * Forwards on every intent, including `content` — unlike the nginx | |
| 175 | + * adapter, which stands down there. See `forward_nginx_helper()` for why | |
| 176 | + * the two differ. | |
| 177 | + * | |
| 108 | 178 | * @param array<string,mixed> $context Public purge context. |
| 109 | 179 | */ |
| 110 | 180 | private static function forward_litespeed( array $context ): void { |
| 111 | 181 | if ( ! defined( 'LSCWP_V' ) ) { |
| @@ -139,8 +209,9 @@ | ||
| 139 | 209 | if ( 'network' === $scope && ! defined( 'LSWCP_EMPTYCACHE' ) ) { |
| 140 | 210 | define( 'LSWCP_EMPTYCACHE', true ); |
| 141 | 211 | } |
| 142 | 212 | \LiteSpeed\Purge::purge_all_lscache( 'xSpeed response invalidation' ); |
| 213 | + self::note_forwarded( 'LiteSpeed Cache' ); | |
| 143 | 214 | } |
| 144 | 215 | return; |
| 145 | 216 | } |
| 146 | 217 | |
| @@ -156,8 +227,394 @@ | ||
| 156 | 227 | } |
| 157 | 228 | foreach ( array_values( array_unique( $targets ) ) as $target ) { |
| 158 | 229 | do_action( 'litespeed_purge_url', $target ); |
| 159 | 230 | } |
| 231 | + if ( array() !== $targets ) { | |
| 232 | + self::note_forwarded( 'LiteSpeed Cache' ); | |
| 233 | + } | |
| 234 | + } | |
| 235 | + | |
| 236 | + /** | |
| 237 | + * nginx FastCGI full-page cache, through the Nginx Helper plugin. | |
| 238 | + * | |
| 239 | + * Forwards on every intent, and on `content` only when Nginx Helper is not | |
| 240 | + * purging for itself or `xspeed_nginx_helper_defer_content_purge` says | |
| 241 | + * to. That asymmetry with `forward_litespeed()`, which forwards on all of | |
| 242 | + * them, is deliberate. | |
| 243 | + * | |
| 244 | + * The two server caches are not alike in what a purge costs. LSCache is | |
| 245 | + * per-site and tag-based: a site purge bumps one tag for one blog. The | |
| 246 | + * nginx FastCGI zone is ONE directory per WordPress install, and clearing | |
| 247 | + * it is a recursive unlink of every cached page — on multisite, of every | |
| 248 | + * site on the network. So the blast radius of forwarding is an order of | |
| 249 | + * magnitude apart for the same event. | |
| 250 | + * | |
| 251 | + * The other half is that we are not the only one purging. Nginx Helper | |
| 252 | + * hooks `transition_post_status`, `before_delete_post` and the comment | |
| 253 | + * hooks itself and purges only the URLs the edit touched (the post, the | |
| 254 | + * homepage, the post's archives), behind its own `enable_purge` option and | |
| 255 | + * an import guard. Its term hooks purge the homepage alone, which is why | |
| 256 | + * a renamed or deleted term is `presentation` and still forwards. On a content | |
| 257 | + * purge it has already done the narrow, correct thing. Forwarding on top | |
| 258 | + * of that replaced targeted purging with a whole-install wipe at the same | |
| 259 | + * frequency: publishing one post cleared every cached page on the site, | |
| 260 | + * and an import cost one full wipe per post. (QA #444.) | |
| 261 | + * | |
| 262 | + * That argument only holds while Nginx Helper's `enable_purge` is on. It | |
| 263 | + * defaults to off, and with it off Nginx Helper purges nothing on a | |
| 264 | + * content edit. Standing down there left the edited post stale at the | |
| 265 | + * server for the whole TTL, where before this adapter existed it was | |
| 266 | + * cleared. So a content purge forwards when Nginx Helper is not purging | |
| 267 | + * for itself. (QA #448) | |
| 268 | + * | |
| 269 | + * The trade that stays: Nginx Helper purges the post, the homepage and | |
| 270 | + * the post's archives. An ordinary page that lists recent posts is none of | |
| 271 | + * those, and keeps its old list until the server TTL expires. When xSpeed | |
| 272 | + * knows that happened, because a narrow purge fell back to the whole site | |
| 273 | + * for a list the theme draws everywhere, the context says so in | |
| 274 | + * `fallback` and the zone is cleared (fallback_needs_whole_zone()). The | |
| 275 | + * `xspeed_nginx_helper_defer_content_purge` filter returns to clearing | |
| 276 | + * the whole zone on every content purge outside an import, for a site that | |
| 277 | + * needs those pages current. | |
| 278 | + * | |
| 279 | + * `presentation` and `complete` still forward, because neither of those is | |
| 280 | + * something Nginx Helper covers. It has no hook for `switch_theme`, | |
| 281 | + * `activated_plugin` or `wp_update_nav_menu`, and no notion of a settings | |
| 282 | + * write or a core update — and each of those changes the markup of every | |
| 283 | + * page, not a listed few. An unrecognised intent forwards too: a purge | |
| 284 | + * whose reason we do not know is likelier to need the server layer than | |
| 285 | + * not, and a redundant purge costs a cold cache while a skipped one costs | |
| 286 | + * wrong HTML for the whole TTL. | |
| 287 | + * | |
| 288 | + * Whether LiteSpeed should also stand down on `content` is a fair question | |
| 289 | + * and was deliberately not revisited here — it has no targeted self-purge | |
| 290 | + * to fall back on, so standing it down would leave LSCache stale where | |
| 291 | + * nginx is merely over-cleared. | |
| 292 | + * | |
| 293 | + * @param array<string,mixed> $context Public purge context. | |
| 294 | + */ | |
| 295 | + private static function forward_nginx_helper( array $context ): void { | |
| 296 | + // Guarded rather than assumed: Free is upgraded as a unit, but a | |
| 297 | + // half-copied update can leave this file newer than that one. | |
| 298 | + if ( ! class_exists( __NAMESPACE__ . '\\Host_Page_Caches' ) ) { | |
| 299 | + return; | |
| 300 | + } | |
| 301 | + | |
| 302 | + $url = isset( $context['url'] ) && is_string( $context['url'] ) ? $context['url'] : ''; | |
| 303 | + $scope = isset( $context['scope'] ) && is_string( $context['scope'] ) | |
| 304 | + ? $context['scope'] | |
| 305 | + : ( '' !== $url ? 'urls' : 'site' ); | |
| 306 | + | |
| 307 | + if ( 'none' === $scope ) { | |
| 308 | + return; | |
| 309 | + } | |
| 310 | + | |
| 311 | + // Named pages go to Nginx Helper's own per-URL purge, whether or not | |
| 312 | + // its automatic purging is on. Its own rules clear the post, the home | |
| 313 | + // page and the post's archives, but no archive page past the first and | |
| 314 | + // no neighbouring post, so leaving a narrow purge to it would leave | |
| 315 | + // those stale. Only this site's URLs: with the `get_request` method | |
| 316 | + // Nginx Helper keeps the path and swaps in its own host. Collected | |
| 317 | + // and sent once, at shutdown (flush_nginx_helper()). | |
| 318 | + if ( 'urls' === $scope ) { | |
| 319 | + if ( self::is_importing() || ! Host_Page_Caches::nginx_helper_is_fastcgi() ) { | |
| 320 | + return; | |
| 321 | + } | |
| 322 | + $urls = isset( $context['urls'] ) && is_array( $context['urls'] ) ? $context['urls'] : array( $url ); | |
| 323 | + foreach ( $urls as $target ) { | |
| 324 | + if ( is_string( $target ) && '' !== $target && self::is_this_site( $target ) ) { | |
| 325 | + self::queue_nginx_url( $target ); | |
| 326 | + } | |
| 327 | + } | |
| 328 | + return; | |
| 329 | + } | |
| 330 | + | |
| 331 | + $intent = isset( $context['intent'] ) && is_string( $context['intent'] ) && '' !== $context['intent'] | |
| 332 | + ? $context['intent'] | |
| 333 | + : 'complete'; | |
| 334 | + | |
| 335 | + // Nothing to decide on a site with no nginx zone, so the filter below | |
| 336 | + // is only asked when there is one. | |
| 337 | + if ( ! Host_Page_Caches::nginx_helper_is_fastcgi() ) { | |
| 338 | + return; | |
| 339 | + } | |
| 340 | + | |
| 341 | + if ( 'content' === $intent ) { | |
| 342 | + /** | |
| 343 | + * Whether a content purge (a post saved, a comment approved, a | |
| 344 | + * term added) is left to Nginx Helper instead of clearing the | |
| 345 | + * whole nginx cache. | |
| 346 | + * | |
| 347 | + * Defaults to true when Nginx Helper's automatic purging is on, | |
| 348 | + * since it has already purged the post, the homepage and the | |
| 349 | + * post's archives. Defaults to false when `fallback` in the | |
| 350 | + * context says pages outside those changed (`theme_list`, | |
| 351 | + * `pending`, `filter`, `listing`). Return false to clear the | |
| 352 | + * whole zone instead, for a site whose pages list posts | |
| 353 | + * somewhere Nginx Helper does not purge. | |
| 354 | + * | |
| 355 | + * @param bool $defer Whether to leave it to Nginx Helper. | |
| 356 | + * @param array<string,mixed> $context Public purge context. | |
| 357 | + */ | |
| 358 | + $defer = (bool) apply_filters( | |
| 359 | + 'xspeed_nginx_helper_defer_content_purge', | |
| 360 | + Host_Page_Caches::nginx_helper_purges_changes() && ! self::fallback_needs_whole_zone( $context ), | |
| 361 | + $context | |
| 362 | + ); | |
| 363 | + if ( $defer ) { | |
| 364 | + return; | |
| 365 | + } | |
| 366 | + } | |
| 367 | + | |
| 368 | + // An import is a long run of legitimate purges that each individually | |
| 369 | + // justify a forward — new terms, new menu items — and together clear | |
| 370 | + // the install's cache hundreds of times for one operation. Nginx | |
| 371 | + // Helper stands its own purging down for exactly this (its | |
| 372 | + // `is_import_request()`), and a single purge after the import is both | |
| 373 | + // cheaper and more correct. An explicit `complete` still goes through: | |
| 374 | + // an operator who presses Purge All mid-import means it. | |
| 375 | + if ( 'complete' !== $intent && self::is_importing() ) { | |
| 376 | + return; | |
| 377 | + } | |
| 378 | + | |
| 379 | + // No host check, deliberately — the mirror of the one in | |
| 380 | + // forward_litespeed(). There, a purge aimed at another blog must not | |
| 381 | + // flush THIS request's LSCache, because LSCache is per-site. nginx | |
| 382 | + // keys one zone per install, so the other blog's cached pages live in | |
| 383 | + // the same directory as ours: skipping on a foreign host would leave | |
| 384 | + // the pages the purge was actually for still being served. Pro's | |
| 385 | + // Multisite::purge_site() runs inside switch_to_blog() and reaches | |
| 386 | + // here with that blog's host. | |
| 387 | + self::purge_nginx_zone(); | |
| 388 | + } | |
| 389 | + | |
| 390 | + /** | |
| 391 | + * Most URLs sent to Nginx Helper one by one in a request. Past this the | |
| 392 | + * zone is cleared once instead. A typical save names about 35 pages. | |
| 393 | + */ | |
| 394 | + private const NGINX_URL_LIMIT = 100; | |
| 395 | + | |
| 396 | + /** | |
| 397 | + * Seconds of per-URL purging after which the rest of the batch becomes | |
| 398 | + * one zone purge. Nginx Helper's `get_request` method sends a blocking | |
| 399 | + * GET per URL with WordPress's 5-second default timeout. | |
| 400 | + */ | |
| 401 | + private const NGINX_URL_SECONDS = 3.0; | |
| 402 | + | |
| 403 | + /** | |
| 404 | + * This request's nginx work, sent once at shutdown. | |
| 405 | + * | |
| 406 | + * `urls` is keyed by URL, so a page that two saves in one request both | |
| 407 | + * name (a bulk edit, where every post shares the home page and the | |
| 408 | + * archives) is sent once. `overflow` means more than the limit arrived. | |
| 409 | + * `zone_done` means the zone was cleared earlier in this request, and | |
| 410 | + * `zone_again` that another clear was asked for after it. | |
| 411 | + * | |
| 412 | + * @var array{urls:array<string,bool>,overflow:bool,zone_done:bool,zone_again:bool,armed:bool} | |
| 413 | + */ | |
| 414 | + private static $nginx = array( | |
| 415 | + 'urls' => array(), | |
| 416 | + 'overflow' => false, | |
| 417 | + 'zone_done' => false, | |
| 418 | + 'zone_again' => false, | |
| 419 | + 'armed' => false, | |
| 420 | + ); | |
| 421 | + | |
| 422 | + /** Test seam: forget this request's nginx work. */ | |
| 423 | + public static function reset(): void { | |
| 424 | + self::$nginx = array( | |
| 425 | + 'urls' => array(), | |
| 426 | + 'overflow' => false, | |
| 427 | + 'zone_done' => false, | |
| 428 | + 'zone_again' => false, | |
| 429 | + 'armed' => false, | |
| 430 | + ); | |
| 431 | + } | |
| 432 | + | |
| 433 | + /** | |
| 434 | + * Clear the whole nginx zone, at most once now and once more at the end | |
| 435 | + * of the request. | |
| 436 | + * | |
| 437 | + * The first clear runs at once, as it always has, so an operator's Purge | |
| 438 | + * All lands before the response. A later one in the same request (a bulk | |
| 439 | + * edit of ten posts that each fall back to the whole site) waits for | |
| 440 | + * shutdown and runs once, after every change the request makes. Clearing | |
| 441 | + * the zone covers every URL queued before it, so those are dropped. | |
| 442 | + */ | |
| 443 | + private static function purge_nginx_zone(): void { | |
| 444 | + self::note_forwarded( 'Nginx Helper' ); | |
| 445 | + self::$nginx['urls'] = array(); | |
| 446 | + self::$nginx['overflow'] = false; | |
| 447 | + if ( self::$nginx['zone_done'] ) { | |
| 448 | + self::$nginx['zone_again'] = true; | |
| 449 | + self::arm_nginx_flush(); | |
| 450 | + return; | |
| 451 | + } | |
| 452 | + if ( Host_Page_Caches::purge_nginx_helper() ) { | |
| 453 | + self::$nginx['zone_done'] = true; | |
| 454 | + } | |
| 455 | + } | |
| 456 | + | |
| 457 | + /** | |
| 458 | + * Tell an operator's purge report which cache took the purge. The | |
| 459 | + * nginx batch is sent at shutdown, after the caller has printed its | |
| 460 | + * answer, so this records the hand-off rather than the send. | |
| 461 | + * | |
| 462 | + * @param string $layer Cache name. | |
| 463 | + */ | |
| 464 | + private static function note_forwarded( string $layer ): void { | |
| 465 | + if ( class_exists( __NAMESPACE__ . '\\Cache' ) ) { | |
| 466 | + Cache::note_purge_forwarded( $layer ); | |
| 467 | + } | |
| 468 | + } | |
| 469 | + | |
| 470 | + /** Add one URL to this request's nginx batch. */ | |
| 471 | + private static function queue_nginx_url( string $url ): void { | |
| 472 | + self::note_forwarded( 'Nginx Helper' ); | |
| 473 | + if ( self::$nginx['zone_again'] ) { | |
| 474 | + // The zone is cleared at shutdown anyway. | |
| 475 | + return; | |
| 476 | + } | |
| 477 | + if ( ! self::$nginx['overflow'] ) { | |
| 478 | + self::$nginx['urls'][ $url ] = true; | |
| 479 | + if ( count( self::$nginx['urls'] ) > self::nginx_url_limit() ) { | |
| 480 | + self::$nginx['overflow'] = true; | |
| 481 | + self::$nginx['urls'] = array(); | |
| 482 | + } | |
| 483 | + } | |
| 484 | + self::arm_nginx_flush(); | |
| 485 | + } | |
| 486 | + | |
| 487 | + /** | |
| 488 | + * Send the batch at shutdown, or now when shutdown is already running. | |
| 489 | + * | |
| 490 | + * Inside `shutdown` a callback added at a priority that has already run | |
| 491 | + * would never fire, so a purge raised there (Cache::flush_pending_saves() | |
| 492 | + * runs at priority 1) is sent straight away. | |
| 493 | + */ | |
| 494 | + private static function arm_nginx_flush(): void { | |
| 495 | + if ( function_exists( 'did_action' ) && did_action( 'shutdown' ) ) { | |
| 496 | + self::flush_nginx_helper(); | |
| 497 | + return; | |
| 498 | + } | |
| 499 | + if ( self::$nginx['armed'] || ! function_exists( 'add_action' ) ) { | |
| 500 | + return; | |
| 501 | + } | |
| 502 | + self::$nginx['armed'] = true; | |
| 503 | + add_action( 'shutdown', array( __CLASS__, 'flush_nginx_helper' ), 20, 0 ); | |
| 504 | + } | |
| 505 | + | |
| 506 | + /** | |
| 507 | + * Send this request's nginx work: one zone clear, or each queued URL. | |
| 508 | + * | |
| 509 | + * Sent in the request that raised it, not handed to WP-Cron the way the | |
| 510 | + * Cloudflare module defers its edge calls. On a site behind an nginx page | |
| 511 | + * cache, anonymous visits are answered by nginx and never run PHP, so | |
| 512 | + * WP-Cron can go a long time without a request to run on, and the pages | |
| 513 | + * would stay stale until it did. The visitor requests that reach here (a | |
| 514 | + * comment, a stock change at checkout) name about four URLs, and Nginx | |
| 515 | + * Helper's own comment and post hooks already purge inline in those same | |
| 516 | + * requests. NGINX_URL_SECONDS bounds the wait when the purge endpoint is | |
| 517 | + * slow. | |
| 518 | + * | |
| 519 | + * Public because it is a `shutdown` callback; not part of the contract. | |
| 520 | + */ | |
| 521 | + public static function flush_nginx_helper(): void { | |
| 522 | + $work = self::$nginx; | |
| 523 | + self::$nginx['urls'] = array(); | |
| 524 | + self::$nginx['overflow'] = false; | |
| 525 | + self::$nginx['zone_again'] = false; | |
| 526 | + self::$nginx['armed'] = false; | |
| 527 | + | |
| 528 | + if ( ! Host_Page_Caches::nginx_helper_is_fastcgi() ) { | |
| 529 | + return; | |
| 530 | + } | |
| 531 | + if ( $work['zone_again'] ) { | |
| 532 | + Host_Page_Caches::purge_nginx_helper(); | |
| 533 | + return; | |
| 534 | + } | |
| 535 | + if ( $work['overflow'] ) { | |
| 536 | + self::record_nginx_zone_fallback(); | |
| 537 | + if ( Host_Page_Caches::purge_nginx_helper() ) { | |
| 538 | + self::$nginx['zone_done'] = true; | |
| 539 | + } | |
| 540 | + return; | |
| 541 | + } | |
| 542 | + if ( array() === $work['urls'] ) { | |
| 543 | + return; | |
| 544 | + } | |
| 545 | + Host_Page_Caches::purge_nginx_helper_urls( array_keys( $work['urls'] ), self::NGINX_URL_SECONDS ); | |
| 546 | + } | |
| 547 | + | |
| 548 | + /** | |
| 549 | + * How many URLs a request may send to Nginx Helper one by one. | |
| 550 | + */ | |
| 551 | + private static function nginx_url_limit(): int { | |
| 552 | + if ( ! function_exists( 'apply_filters' ) ) { | |
| 553 | + return self::NGINX_URL_LIMIT; | |
| 554 | + } | |
| 555 | + /** | |
| 556 | + * Filter how many URLs one request sends to Nginx Helper one by one | |
| 557 | + * before clearing the whole nginx zone instead. | |
| 558 | + * | |
| 559 | + * @param int $limit URLs per request. | |
| 560 | + */ | |
| 561 | + $limit = (int) apply_filters( 'xspeed_nginx_helper_url_purge_limit', self::NGINX_URL_LIMIT ); | |
| 562 | + return $limit > 0 ? $limit : self::NGINX_URL_LIMIT; | |
| 563 | + } | |
| 564 | + | |
| 565 | + /** Say in the activity log that a batch became a zone clear. */ | |
| 566 | + private static function record_nginx_zone_fallback(): void { | |
| 567 | + if ( ! class_exists( __NAMESPACE__ . '\\Activity_Log' ) || ! function_exists( '__' ) ) { | |
| 568 | + return; | |
| 569 | + } | |
| 570 | + Activity_Log::record( | |
| 571 | + 'cache_purged', | |
| 572 | + sprintf( | |
| 573 | + /* translators: %d: most URLs purged one by one. */ | |
| 574 | + __( 'Cleared the whole nginx cache: more than %d pages changed in one request, too many to purge one by one', 'xspeed' ), | |
| 575 | + self::nginx_url_limit() | |
| 576 | + ) | |
| 577 | + ); | |
| 578 | + } | |
| 579 | + | |
| 580 | + /** | |
| 581 | + * Whether a site-wide content purge fell back from a narrow one for a | |
| 582 | + * reason Nginx Helper's own rules do not cover. | |
| 583 | + * | |
| 584 | + * Nginx Helper purges the post, the homepage and the first page of the | |
| 585 | + * post's archives. That is enough for `limit`: the pages were all named, | |
| 586 | + * only too many of them, and the ones past the first archive page wait | |
| 587 | + * for the server TTL, as every content purge did before narrow purges. | |
| 588 | + * It is not enough for the others: | |
| 589 | + * | |
| 590 | + * - `theme_list`: a list the theme draws on every page changed. | |
| 591 | + * - `pending`: the save could not be worked out at all. | |
| 592 | + * - `filter`: a site's own code said the named pages are not enough. | |
| 593 | + * - `listing`: pages that run a post list of their own (a page builder | |
| 594 | + * grid) may have changed, and they are not ones Nginx Helper purges. | |
| 595 | + * | |
| 596 | + * An excluded post type (a WooCommerce product) carries no reason and | |
| 597 | + * stays with Nginx Helper. | |
| 598 | + * | |
| 599 | + * @param array<string,mixed> $context Public purge context. | |
| 600 | + */ | |
| 601 | + private static function fallback_needs_whole_zone( array $context ): bool { | |
| 602 | + $reason = isset( $context['fallback'] ) && is_string( $context['fallback'] ) ? $context['fallback'] : ''; | |
| 603 | + return in_array( $reason, array( Cache::FALLBACK_THEME_LIST, Cache::FALLBACK_PENDING, Cache::FALLBACK_FILTER, Cache::FALLBACK_LISTING ), true ); | |
| 604 | + } | |
| 605 | + | |
| 606 | + /** | |
| 607 | + * Whether WordPress is importing content right now. | |
| 608 | + */ | |
| 609 | + private static function is_importing(): bool { | |
| 610 | + if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) { | |
| 611 | + return true; | |
| 612 | + } | |
| 613 | + | |
| 614 | + // The WXR importer defines WP_IMPORTING, but not every importer does; | |
| 615 | + // `import_start` is the signal the others share. | |
| 616 | + return function_exists( 'did_action' ) && did_action( 'import_start' ) > 0; | |
| 160 | 617 | } |
| 161 | 618 | |
| 162 | 619 | /** |
| 163 | 620 | * Build LiteSpeed targets for one same-site URL. |