| @@ -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 */ |
| @@ -76,8 +76,12 @@ | ||
| 76 | 76 | // Per-post cache rules (Phase 3.4) — registers postmeta with |
| 77 | 77 | // REST + meta box on edit screens. |
| 78 | 78 | Cache_Meta_Box::boot(); |
| 79 | 79 | |
| 80 | + // Single-URL purge entry points — row actions, the edit-screen | |
| 81 | + // button and the admin-post handler the admin-bar item also uses. | |
| 82 | + Purge_Ui::boot(); | |
| 83 | + | |
| 80 | 84 | // Phase 0 architecture — managers + Free modules. v1 services |
| 81 | 85 | // (Cache/Minifier/Gzip) are NOT yet Modules; they'll be refactored |
| 82 | 86 | // in a follow-up PR with parity tests. |
| 83 | 87 | Conflict_Registry::boot(); |
| @@ -89,9 +93,30 @@ | ||
| 89 | 93 | // Integrations that clear OTHER plugins' caches of rendered output |
| 90 | 94 | // (Elementor's element cache + generated CSS). Registers a listener |
| 91 | 95 | // only; nothing runs until Cache::purge_render_caches() asks. |
| 92 | 96 | Render_Caches::boot(); |
| 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(); | |
| 93 | 106 | |
| 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(); | |
| 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 | + | |
| 94 | 119 | // Register Free modules via the same action xspeed-pro uses, so |
| 95 | 120 | // the bootstrap path is symmetric across tiers. |
| 96 | 121 | add_action( 'xspeed_register_modules', array( $this, 'register_free_modules' ) ); |
| 97 | 122 | |
| @@ -116,8 +141,38 @@ | ||
| 116 | 141 | } |
| 117 | 142 | } |
| 118 | 143 | |
| 119 | 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 | + /** | |
| 120 | 175 | * Run version-gated data migrations exactly once per upgrade. |
| 121 | 176 | * |
| 122 | 177 | * Keyed on `xspeed_data_version` rather than the plugin version header |
| 123 | 178 | * so a migration can be added without forcing a release bump. |
| @@ -127,17 +182,25 @@ | ||
| 127 | 182 | if ( version_compare( $current, self::DATA_VERSION, '>=' ) ) { |
| 128 | 183 | return; |
| 129 | 184 | } |
| 130 | 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 | + | |
| 131 | 190 | // 1.1.2 — strip credential values recorded by earlier versions' |
| 132 | 191 | // settings change annotations (they're served by the trend endpoints). |
| 133 | - Activity_Log::redact_legacy_secrets(); | |
| 192 | + if ( version_compare( $current, '1.1.2', '<' ) ) { | |
| 193 | + Activity_Log::redact_legacy_secrets(); | |
| 194 | + } | |
| 134 | 195 | |
| 135 | 196 | // 1.1.4 — earlier versions cached a failed loopback as "gzip is not |
| 136 | 197 | // active" for an hour, which showed up as a bogus server-config |
| 137 | 198 | // warning. Drop the stale answer so the fixed probe re-runs instead |
| 138 | 199 | // of the wrong verdict living on past the update (issue #18). |
| 139 | - delete_transient( 'xspeed_gzip_active' ); | |
| 200 | + if ( version_compare( $current, '1.1.4', '<' ) ) { | |
| 201 | + delete_transient( 'xspeed_gzip_active' ); | |
| 202 | + } | |
| 140 | 203 | |
| 141 | 204 | // 1.1.6 — a `/?s=<term>` request used to write its results page into |
| 142 | 205 | // the static tree under the *searched-from* path, which for the usual |
| 143 | 206 | // query-form search is `/`. The web server then served that results |
| @@ -145,10 +208,50 @@ | ||
| 145 | 208 | // Cache::store_static(), but an entry poisoned before the update |
| 146 | 209 | // outlives it: nothing purges on upgrade, and the static serve path |
| 147 | 210 | // never revalidates. Clear the tree once. The flat cache is keyed |
| 148 | 211 | // correctly and is deliberately left alone. (issue #191) |
| 149 | - Cache::purge_static_tree(); | |
| 212 | + if ( version_compare( $current, '1.1.6', '<' ) ) { | |
| 213 | + $removed = Cache::purge_static_tree(); | |
| 150 | 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 | + | |
| 151 | 254 | update_option( 'xspeed_data_version', self::DATA_VERSION, false ); |
| 152 | 255 | } |
| 153 | 256 | |
| 154 | 257 | /** |
| @@ -256,8 +359,9 @@ | ||
| 256 | 359 | // cache-coverage features (404 / search / feed / REST / rules / |
| 257 | 360 | // maintenance) into one sidebar sub-item (FBS-83633). |
| 258 | 361 | Module_Registry::register( new \XSpeed\Modules\CacheCoverage\CacheCoverageModule() ); |
| 259 | 362 | Module_Registry::register( new \XSpeed\Modules\Fonts\FontsModule() ); |
| 363 | + Module_Registry::register( new \XSpeed\Modules\TurboRender\TurboRenderModule() ); | |
| 260 | 364 | Module_Registry::register( new \XSpeed\Modules\ResourceHints\ResourceHintsModule() ); |
| 261 | 365 | // AI Privacy (GDPR off-switch) ships in Free even though every AI |
| 262 | 366 | // *feature* is Pro — privacy is a right, not a paid tier. FEATURES.md |
| 263 | 367 | // §AI row 6 mandates it. Without this registration the module was dead |
| @@ -274,8 +378,12 @@ | ||
| 274 | 378 | // snapshot is onboarding/diagnostics, not a paid value-add, so every |
| 275 | 379 | // user gets it. The snapshot degrades gracefully without Pro (Pro |
| 276 | 380 | // version/license fields fall back to defaults via defined()/get_option). |
| 277 | 381 | Module_Registry::register( new \XSpeed\Modules\Support\SupportModule() ); |
| 382 | + // The dashboard control for usage-analytics consent. Consent used to | |
| 383 | + // be collectable only in the wizard and withdrawable nowhere, while | |
| 384 | + // the wizard and readme both promised a dashboard switch. (#437) | |
| 385 | + Module_Registry::register( new \XSpeed\Modules\Privacy\PrivacyModule() ); | |
| 278 | 386 | // MCP remote control (AI assistants) — Free. The plugin serves the |
| 279 | 387 | // MCP protocol at the site's own /xspeed/mcp URL; the only gate is |
| 280 | 388 | // the per-site connection token an admin mints via Connect. No |
| 281 | 389 | // license, no hosted infra. See IMPLEMENTATION.md §17. |
| @@ -290,10 +398,11 @@ | ||
| 290 | 398 | public function start_plugin_tracking(): void { |
| 291 | 399 | $this->usage_tracker = Usage_Tracker::get_instance( |
| 292 | 400 | XSPEED_FILE, |
| 293 | 401 | array( |
| 294 | - 'opt_in' => true, | |
| 295 | - 'item_id' => defined( 'XSPEED_INSIGHTS_ITEM_ID' ) ? XSPEED_INSIGHTS_ITEM_ID : false, | |
| 402 | + 'opt_in' => true, | |
| 403 | + 'email_marketing' => true, | |
| 404 | + 'item_id' => defined( 'XSPEED_INSIGHTS_ITEM_ID' ) ? XSPEED_INSIGHTS_ITEM_ID : false, | |
| 296 | 405 | ) |
| 297 | 406 | ); |
| 298 | 407 | $this->usage_tracker->init(); |
| 299 | 408 | } |
| @@ -419,8 +528,22 @@ | ||
| 419 | 528 | * @param string $profile Settings::PROFILE_* — which profile a fresh |
| 420 | 529 | * install came up with, '' if not fresh. |
| 421 | 530 | */ |
| 422 | 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' ); | |
| 423 | 546 | } |
| 424 | 547 | |
| 425 | 548 | /** |
| 426 | 549 | * Restore the cache drop-in right after THIS plugin is updated. |
| @@ -463,9 +586,14 @@ | ||
| 463 | 586 | |
| 464 | 587 | Cache::restore_dropin_if_enabled(); |
| 465 | 588 | } |
| 466 | 589 | |
| 467 | - 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 ) { | |
| 468 | 596 | // Drop-in + WP_CACHE constant are NOT touched here. WordPress |
| 469 | 597 | // upgrades run as deactivate → wipe files → install → activate, |
| 470 | 598 | // so removing those artifacts on every deactivate would silently |
| 471 | 599 | // disable caching after each plugin update. uninstall.php |
| @@ -472,9 +600,16 @@ | ||
| 472 | 600 | // handles full teardown when the user actually removes the |
| 473 | 601 | // plugin; auto_heal() restores state on the next admin_init if |
| 474 | 602 | // the drop-in or WP_CACHE went missing for any other reason. |
| 475 | 603 | Cache::purge_all(); |
| 476 | - 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 | + } | |
| 477 | 612 | Gzip::apply( false ); |
| 478 | 613 | |
| 479 | 614 | Module_Registry::deactivate_all(); |
| 480 | 615 | } |