| @@ -14,9 +14,9 @@ | ||
| 14 | 14 | /** |
| 15 | 15 | * Data-schema version for one-time migrations, independent of the |
| 16 | 16 | * plugin version header. Bump when adding a step to maybe_upgrade(). |
| 17 | 17 | */ |
| 18 | - public const DATA_VERSION = '1.1.6'; | |
| 18 | + public const DATA_VERSION = '1.3.6'; | |
| 19 | 19 | |
| 20 | 20 | private static $instance = null; |
| 21 | 21 | |
| 22 | 22 | /** @var Usage_Tracker|null */ |
| @@ -93,17 +93,30 @@ | ||
| 93 | 93 | // Integrations that clear OTHER plugins' caches of rendered output |
| 94 | 94 | // (Elementor's element cache + generated CSS). Registers a listener |
| 95 | 95 | // only; nothing runs until Cache::purge_render_caches() asks. |
| 96 | 96 | Render_Caches::boot(); |
| 97 | - // Server_Caches needs no boot(): Cache::dispatch_purge_event() calls it | |
| 98 | - // directly so a throwing third-party listener cannot skip it. | |
| 97 | + // Forwarding needs no boot(): Cache::dispatch_purge_event() calls | |
| 98 | + // Server_Caches::forward() directly so a throwing third-party listener | |
| 99 | + // cannot skip it. Both built-in server-cache adapters live behind it — | |
| 100 | + // LiteSpeed, and the nginx FastCGI cache reached through the host's | |
| 101 | + // Nginx Helper install (Host_Page_Caches), which is why neither is | |
| 102 | + // booted here. This boot() registers one listener only: the single | |
| 103 | + // purge that runs after an import, which the nginx adapter's import | |
| 104 | + // gate stands down in favour of. | |
| 105 | + Server_Caches::boot(); | |
| 99 | 106 | |
| 100 | - // Forward a full purge to a full-page cache owned by the WEB SERVER | |
| 101 | - // (nginx FastCGI, via the host's Nginx Helper install). Registers a | |
| 102 | - // listener on xspeed_after_purge_all only; it stands down unless | |
| 103 | - // that cache is actually configured. | |
| 104 | - Host_Page_Caches::boot(); | |
| 107 | + // Remembers the terms a post had before a save, so a narrow purge | |
| 108 | + // can clear the category it left as well as the one it joined. | |
| 109 | + Affected_Pages::boot(); | |
| 110 | + // Records which pages run a post list of their own (a page builder | |
| 111 | + // grid, a related-posts block), so a narrow purge clears them too. | |
| 112 | + Listing_Pages::boot(); | |
| 105 | 113 | |
| 114 | + // Tell WP Statistics and Slimstat not to count the warmer, the | |
| 115 | + // benchmark and the verifier as visitors. Record-time filters only; | |
| 116 | + // see the class for why the tracking snippet itself is left alone. | |
| 117 | + Self_Traffic::boot(); | |
| 118 | + | |
| 106 | 119 | // Register Free modules via the same action xspeed-pro uses, so |
| 107 | 120 | // the bootstrap path is symmetric across tiers. |
| 108 | 121 | add_action( 'xspeed_register_modules', array( $this, 'register_free_modules' ) ); |
| 109 | 122 | |
| @@ -128,8 +141,38 @@ | ||
| 128 | 141 | } |
| 129 | 142 | } |
| 130 | 143 | |
| 131 | 144 | /** |
| 145 | + * Add the 1.3.6 tracking params to a saved ignored-params list. | |
| 146 | + * | |
| 147 | + * Only those names, and only when missing: anything else the site | |
| 148 | + * removed stays removed. A site with no saved list already reads the new | |
| 149 | + * defaults. | |
| 150 | + */ | |
| 151 | + public static function add_new_ignored_params(): void { | |
| 152 | + $option = Settings_Manager::OPTION_PREFIX . 'cache'; | |
| 153 | + $stored = get_option( $option, array() ); | |
| 154 | + if ( ! is_array( $stored ) || ! is_array( $stored['ignored_query_params'] ?? null ) ) { | |
| 155 | + return; | |
| 156 | + } | |
| 157 | + $list = $stored['ignored_query_params']; | |
| 158 | + // An empty list is a choice: every query string bypasses the cache. | |
| 159 | + if ( empty( $list ) ) { | |
| 160 | + return; | |
| 161 | + } | |
| 162 | + $missing = array_values( array_diff( \XSpeed\Modules\Cache\CacheModule::TRACKING_PARAMS_1_3_6, $list ) ); | |
| 163 | + if ( empty( $missing ) ) { | |
| 164 | + return; | |
| 165 | + } | |
| 166 | + $stored['ignored_query_params'] = array_merge( $list, $missing ); | |
| 167 | + update_option( $option, $stored ); | |
| 168 | + // The drop-in keeps its own copy of the list next to the cache. | |
| 169 | + if ( defined( 'XSPEED_CACHE_DIR' ) ) { | |
| 170 | + Cache::sync_query_allowlist(); | |
| 171 | + } | |
| 172 | + } | |
| 173 | + | |
| 174 | + /** | |
| 132 | 175 | * Run version-gated data migrations exactly once per upgrade. |
| 133 | 176 | * |
| 134 | 177 | * Keyed on `xspeed_data_version` rather than the plugin version header |
| 135 | 178 | * so a migration can be added without forcing a release bump. |
| @@ -139,17 +182,25 @@ | ||
| 139 | 182 | if ( version_compare( $current, self::DATA_VERSION, '>=' ) ) { |
| 140 | 183 | return; |
| 141 | 184 | } |
| 142 | 185 | |
| 186 | + // Each step runs once, for the version that needs it. They used to | |
| 187 | + // run on every bump, which would purge every site's static tree again | |
| 188 | + // for a migration that has nothing to do with it. | |
| 189 | + | |
| 143 | 190 | // 1.1.2 — strip credential values recorded by earlier versions' |
| 144 | 191 | // settings change annotations (they're served by the trend endpoints). |
| 145 | - Activity_Log::redact_legacy_secrets(); | |
| 192 | + if ( version_compare( $current, '1.1.2', '<' ) ) { | |
| 193 | + Activity_Log::redact_legacy_secrets(); | |
| 194 | + } | |
| 146 | 195 | |
| 147 | 196 | // 1.1.4 — earlier versions cached a failed loopback as "gzip is not |
| 148 | 197 | // active" for an hour, which showed up as a bogus server-config |
| 149 | 198 | // warning. Drop the stale answer so the fixed probe re-runs instead |
| 150 | 199 | // of the wrong verdict living on past the update (issue #18). |
| 151 | - delete_transient( 'xspeed_gzip_active' ); | |
| 200 | + if ( version_compare( $current, '1.1.4', '<' ) ) { | |
| 201 | + delete_transient( 'xspeed_gzip_active' ); | |
| 202 | + } | |
| 152 | 203 | |
| 153 | 204 | // 1.1.6 — a `/?s=<term>` request used to write its results page into |
| 154 | 205 | // the static tree under the *searched-from* path, which for the usual |
| 155 | 206 | // query-form search is `/`. The web server then served that results |
| @@ -157,10 +208,50 @@ | ||
| 157 | 208 | // Cache::store_static(), but an entry poisoned before the update |
| 158 | 209 | // outlives it: nothing purges on upgrade, and the static serve path |
| 159 | 210 | // never revalidates. Clear the tree once. The flat cache is keyed |
| 160 | 211 | // correctly and is deliberately left alone. (issue #191) |
| 161 | - Cache::purge_static_tree(); | |
| 212 | + if ( version_compare( $current, '1.1.6', '<' ) ) { | |
| 213 | + $removed = Cache::purge_static_tree(); | |
| 162 | 214 | |
| 215 | + /* | |
| 216 | + * And tell anything caching in front of us, because the poisoned | |
| 217 | + * entry is exactly the kind that reaches an edge: the web server | |
| 218 | + * served it straight off disk as the homepage, so a CDN in front had | |
| 219 | + * every opportunity to store it. Deleting our copy leaves that one | |
| 220 | + * untouched, and the edge's lifetime is the longer of the two. | |
| 221 | + * | |
| 222 | + * Announced on `init` rather than here. This runs at | |
| 223 | + * `plugins_loaded` 21, before an add-on has wired its purge | |
| 224 | + * listeners, so firing it inline would announce to an empty room. | |
| 225 | + * Priority 99 puts it after any reasonable `init` registration. | |
| 226 | + * | |
| 227 | + * Once per site, on the upgrade that clears the tree — never again, | |
| 228 | + * since the data version is written immediately below. | |
| 229 | + * | |
| 230 | + * Through `Cache::announce_purge()` rather than a bare `do_action`, | |
| 231 | + * because the purge-event contract (#348) is more than the payload: | |
| 232 | + * it forwards to the server caches by direct call before the public | |
| 233 | + * action runs, and it isolates listeners so one throwing add-on | |
| 234 | + * cannot fatal the first admin request after an update. A raw | |
| 235 | + * publish here would skip both and hand listeners the pre-contract | |
| 236 | + * payload shape. | |
| 237 | + */ | |
| 238 | + add_action( | |
| 239 | + 'init', | |
| 240 | + static function () use ( $removed ) { | |
| 241 | + Cache::announce_purge( 'static cache repaired on upgrade', $removed ); | |
| 242 | + }, | |
| 243 | + 99 | |
| 244 | + ); | |
| 245 | + } | |
| 246 | + | |
| 247 | + // 1.3.6 — new click and campaign IDs in the default ignored list. | |
| 248 | + // A site that ever saved the Cache panel has its own copy of the list, | |
| 249 | + // which the new defaults never reach, so add them to it. | |
| 250 | + if ( version_compare( $current, '1.3.6', '<' ) ) { | |
| 251 | + self::add_new_ignored_params(); | |
| 252 | + } | |
| 253 | + | |
| 163 | 254 | update_option( 'xspeed_data_version', self::DATA_VERSION, false ); |
| 164 | 255 | } |
| 165 | 256 | |
| 166 | 257 | /** |
| @@ -437,8 +528,22 @@ | ||
| 437 | 528 | * @param string $profile Settings::PROFILE_* — which profile a fresh |
| 438 | 529 | * install came up with, '' if not fresh. |
| 439 | 530 | */ |
| 440 | 531 | do_action( 'xspeed_activated', $installed_by, $profile ); |
| 532 | + | |
| 533 | + /* | |
| 534 | + * Announce the change to anything caching in front of us. | |
| 535 | + * | |
| 536 | + * Activation writes the default settings and activates modules, so | |
| 537 | + * every URL on the site starts returning different HTML. A CDN | |
| 538 | + * holding renders from before goes on serving them for their whole | |
| 539 | + * lifetime, and nothing told it. | |
| 540 | + * | |
| 541 | + * Cheap on a fresh install — there is nothing to sweep — and the | |
| 542 | + * case it exists for is reactivation on a site that has been running | |
| 543 | + * for months behind an edge. | |
| 544 | + */ | |
| 545 | + Cache::purge_all( 'plugin activated' ); | |
| 441 | 546 | } |
| 442 | 547 | |
| 443 | 548 | /** |
| 444 | 549 | * Restore the cache drop-in right after THIS plugin is updated. |
| @@ -481,9 +586,14 @@ | ||
| 481 | 586 | |
| 482 | 587 | Cache::restore_dropin_if_enabled(); |
| 483 | 588 | } |
| 484 | 589 | |
| 485 | - public static function deactivate() { | |
| 590 | + /** | |
| 591 | + * @param bool $network_wide Whether a network admin deactivated the | |
| 592 | + * plugin for every site (WordPress passes this | |
| 593 | + * to deactivation hooks). | |
| 594 | + */ | |
| 595 | + public static function deactivate( $network_wide = false ) { | |
| 486 | 596 | // Drop-in + WP_CACHE constant are NOT touched here. WordPress |
| 487 | 597 | // upgrades run as deactivate → wipe files → install → activate, |
| 488 | 598 | // so removing those artifacts on every deactivate would silently |
| 489 | 599 | // disable caching after each plugin update. uninstall.php |
| @@ -490,9 +600,16 @@ | ||
| 490 | 600 | // handles full teardown when the user actually removes the |
| 491 | 601 | // plugin; auto_heal() restores state on the next admin_init if |
| 492 | 602 | // the drop-in or WP_CACHE went missing for any other reason. |
| 493 | 603 | Cache::purge_all(); |
| 494 | - Minifier::purge_minified(); | |
| 604 | + // purge_all() is site-scoped and no longer touches min/, so clear it | |
| 605 | + // here. On a network where only this site is deactivating, the other | |
| 606 | + // sites still link those files: drop this site's manifests only. | |
| 607 | + if ( is_multisite() && ! $network_wide ) { | |
| 608 | + Minifier::purge_manifests( Asset_Manifest::blog_id() ); | |
| 609 | + } else { | |
| 610 | + Minifier::purge_minified(); | |
| 611 | + } | |
| 495 | 612 | Gzip::apply( false ); |
| 496 | 613 | |
| 497 | 614 | Module_Registry::deactivate_all(); |
| 498 | 615 | } |