PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.2
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.2
1.4.2 1.4.1 1.4.0 1.3.7 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 All 36 releases
← All changes | includes/class-plugin.php +146 -8 1.2.4 → 1.4.2 View file →
@@ -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,15 @@
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 +
84 + // While the page cache is off, every admin page says so (#594).
85 + Cache_Off_Notice::boot();
86 +
80 87 // Phase 0 architecture — managers + Free modules. v1 services
81 88 // (Cache/Minifier/Gzip) are NOT yet Modules; they'll be refactored
82 89 // in a follow-up PR with parity tests.
83 90 Conflict_Registry::boot();
@@ -89,9 +96,30 @@
89 96 // Integrations that clear OTHER plugins' caches of rendered output
90 97 // (Elementor's element cache + generated CSS). Registers a listener
91 98 // only; nothing runs until Cache::purge_render_caches() asks.
92 99 Render_Caches::boot();
100 + // Forwarding needs no boot(): Cache::dispatch_purge_event() calls
101 + // Server_Caches::forward() directly so a throwing third-party listener
102 + // cannot skip it. Both built-in server-cache adapters live behind it —
103 + // LiteSpeed, and the nginx FastCGI cache reached through the host's
104 + // Nginx Helper install (Host_Page_Caches), which is why neither is
105 + // booted here. This boot() registers one listener only: the single
106 + // purge that runs after an import, which the nginx adapter's import
107 + // gate stands down in favour of.
108 + Server_Caches::boot();
93 109
110 + // Remembers the terms a post had before a save, so a narrow purge
111 + // can clear the category it left as well as the one it joined.
112 + Affected_Pages::boot();
113 + // Records which pages run a post list of their own (a page builder
114 + // grid, a related-posts block), so a narrow purge clears them too.
115 + Listing_Pages::boot();
116 +
117 + // Tell WP Statistics and Slimstat not to count the warmer, the
118 + // benchmark and the verifier as visitors. Record-time filters only;
119 + // see the class for why the tracking snippet itself is left alone.
120 + Self_Traffic::boot();
121 +
94 122 // Register Free modules via the same action xspeed-pro uses, so
95 123 // the bootstrap path is symmetric across tiers.
96 124 add_action( 'xspeed_register_modules', array( $this, 'register_free_modules' ) );
97 125
@@ -116,8 +144,38 @@
116 144 }
117 145 }
118 146
119 147 /**
148 + * Add the 1.3.6 tracking params to a saved ignored-params list.
149 + *
150 + * Only those names, and only when missing: anything else the site
151 + * removed stays removed. A site with no saved list already reads the new
152 + * defaults.
153 + */
154 + public static function add_new_ignored_params(): void {
155 + $option = Settings_Manager::OPTION_PREFIX . 'cache';
156 + $stored = get_option( $option, array() );
157 + if ( ! is_array( $stored ) || ! is_array( $stored['ignored_query_params'] ?? null ) ) {
158 + return;
159 + }
160 + $list = $stored['ignored_query_params'];
161 + // An empty list is a choice: every query string bypasses the cache.
162 + if ( empty( $list ) ) {
163 + return;
164 + }
165 + $missing = array_values( array_diff( \XSpeed\Modules\Cache\CacheModule::TRACKING_PARAMS_1_3_6, $list ) );
166 + if ( empty( $missing ) ) {
167 + return;
168 + }
169 + $stored['ignored_query_params'] = array_merge( $list, $missing );
170 + update_option( $option, $stored );
171 + // The drop-in keeps its own copy of the list next to the cache.
172 + if ( defined( 'XSPEED_CACHE_DIR' ) ) {
173 + Cache::sync_query_allowlist();
174 + }
175 + }
176 +
177 + /**
120 178 * Run version-gated data migrations exactly once per upgrade.
121 179 *
122 180 * Keyed on `xspeed_data_version` rather than the plugin version header
123 181 * so a migration can be added without forcing a release bump.
@@ -127,17 +185,25 @@
127 185 if ( version_compare( $current, self::DATA_VERSION, '>=' ) ) {
128 186 return;
129 187 }
130 188
189 + // Each step runs once, for the version that needs it. They used to
190 + // run on every bump, which would purge every site's static tree again
191 + // for a migration that has nothing to do with it.
192 +
131 193 // 1.1.2 — strip credential values recorded by earlier versions'
132 194 // settings change annotations (they're served by the trend endpoints).
133 - Activity_Log::redact_legacy_secrets();
195 + if ( version_compare( $current, '1.1.2', '<' ) ) {
196 + Activity_Log::redact_legacy_secrets();
197 + }
134 198
135 199 // 1.1.4 — earlier versions cached a failed loopback as "gzip is not
136 200 // active" for an hour, which showed up as a bogus server-config
137 201 // warning. Drop the stale answer so the fixed probe re-runs instead
138 202 // of the wrong verdict living on past the update (issue #18).
139 - delete_transient( 'xspeed_gzip_active' );
203 + if ( version_compare( $current, '1.1.4', '<' ) ) {
204 + delete_transient( 'xspeed_gzip_active' );
205 + }
140 206
141 207 // 1.1.6 — a `/?s=<term>` request used to write its results page into
142 208 // the static tree under the *searched-from* path, which for the usual
143 209 // query-form search is `/`. The web server then served that results
@@ -145,10 +211,50 @@
145 211 // Cache::store_static(), but an entry poisoned before the update
146 212 // outlives it: nothing purges on upgrade, and the static serve path
147 213 // never revalidates. Clear the tree once. The flat cache is keyed
148 214 // correctly and is deliberately left alone. (issue #191)
149 - Cache::purge_static_tree();
215 + if ( version_compare( $current, '1.1.6', '<' ) ) {
216 + $removed = Cache::purge_static_tree();
150 217
218 + /*
219 + * And tell anything caching in front of us, because the poisoned
220 + * entry is exactly the kind that reaches an edge: the web server
221 + * served it straight off disk as the homepage, so a CDN in front had
222 + * every opportunity to store it. Deleting our copy leaves that one
223 + * untouched, and the edge's lifetime is the longer of the two.
224 + *
225 + * Announced on `init` rather than here. This runs at
226 + * `plugins_loaded` 21, before an add-on has wired its purge
227 + * listeners, so firing it inline would announce to an empty room.
228 + * Priority 99 puts it after any reasonable `init` registration.
229 + *
230 + * Once per site, on the upgrade that clears the tree — never again,
231 + * since the data version is written immediately below.
232 + *
233 + * Through `Cache::announce_purge()` rather than a bare `do_action`,
234 + * because the purge-event contract (#348) is more than the payload:
235 + * it forwards to the server caches by direct call before the public
236 + * action runs, and it isolates listeners so one throwing add-on
237 + * cannot fatal the first admin request after an update. A raw
238 + * publish here would skip both and hand listeners the pre-contract
239 + * payload shape.
240 + */
241 + add_action(
242 + 'init',
243 + static function () use ( $removed ) {
244 + Cache::announce_purge( 'static cache repaired on upgrade', $removed );
245 + },
246 + 99
247 + );
248 + }
249 +
250 + // 1.3.6 — new click and campaign IDs in the default ignored list.
251 + // A site that ever saved the Cache panel has its own copy of the list,
252 + // which the new defaults never reach, so add them to it.
253 + if ( version_compare( $current, '1.3.6', '<' ) ) {
254 + self::add_new_ignored_params();
255 + }
256 +
151 257 update_option( 'xspeed_data_version', self::DATA_VERSION, false );
152 258 }
153 259
154 260 /**
@@ -256,8 +362,9 @@
256 362 // cache-coverage features (404 / search / feed / REST / rules /
257 363 // maintenance) into one sidebar sub-item (FBS-83633).
258 364 Module_Registry::register( new \XSpeed\Modules\CacheCoverage\CacheCoverageModule() );
259 365 Module_Registry::register( new \XSpeed\Modules\Fonts\FontsModule() );
366 + Module_Registry::register( new \XSpeed\Modules\TurboRender\TurboRenderModule() );
260 367 Module_Registry::register( new \XSpeed\Modules\ResourceHints\ResourceHintsModule() );
261 368 // AI Privacy (GDPR off-switch) ships in Free even though every AI
262 369 // *feature* is Pro — privacy is a right, not a paid tier. FEATURES.md
263 370 // §AI row 6 mandates it. Without this registration the module was dead
@@ -274,8 +381,12 @@
274 381 // snapshot is onboarding/diagnostics, not a paid value-add, so every
275 382 // user gets it. The snapshot degrades gracefully without Pro (Pro
276 383 // version/license fields fall back to defaults via defined()/get_option).
277 384 Module_Registry::register( new \XSpeed\Modules\Support\SupportModule() );
385 + // The dashboard control for usage-analytics consent. Consent used to
386 + // be collectable only in the wizard and withdrawable nowhere, while
387 + // the wizard and readme both promised a dashboard switch. (#437)
388 + Module_Registry::register( new \XSpeed\Modules\Privacy\PrivacyModule() );
278 389 // MCP remote control (AI assistants) — Free. The plugin serves the
279 390 // MCP protocol at the site's own /xspeed/mcp URL; the only gate is
280 391 // the per-site connection token an admin mints via Connect. No
281 392 // license, no hosted infra. See IMPLEMENTATION.md §17.
@@ -290,10 +401,11 @@
290 401 public function start_plugin_tracking(): void {
291 402 $this->usage_tracker = Usage_Tracker::get_instance(
292 403 XSPEED_FILE,
293 404 array(
294 - 'opt_in' => true,
295 - 'item_id' => defined( 'XSPEED_INSIGHTS_ITEM_ID' ) ? XSPEED_INSIGHTS_ITEM_ID : false,
405 + 'opt_in' => true,
406 + 'email_marketing' => true,
407 + 'item_id' => defined( 'XSPEED_INSIGHTS_ITEM_ID' ) ? XSPEED_INSIGHTS_ITEM_ID : false,
296 408 )
297 409 );
298 410 $this->usage_tracker->init();
299 411 }
@@ -419,8 +531,22 @@
419 531 * @param string $profile Settings::PROFILE_* — which profile a fresh
420 532 * install came up with, '' if not fresh.
421 533 */
422 534 do_action( 'xspeed_activated', $installed_by, $profile );
535 +
536 + /*
537 + * Announce the change to anything caching in front of us.
538 + *
539 + * Activation writes the default settings and activates modules, so
540 + * every URL on the site starts returning different HTML. A CDN
541 + * holding renders from before goes on serving them for their whole
542 + * lifetime, and nothing told it.
543 + *
544 + * Cheap on a fresh install — there is nothing to sweep — and the
545 + * case it exists for is reactivation on a site that has been running
546 + * for months behind an edge.
547 + */
548 + Cache::purge_all( 'plugin activated' );
423 549 }
424 550
425 551 /**
426 552 * Restore the cache drop-in right after THIS plugin is updated.
@@ -463,9 +589,14 @@
463 589
464 590 Cache::restore_dropin_if_enabled();
465 591 }
466 592
467 - public static function deactivate() {
593 + /**
594 + * @param bool $network_wide Whether a network admin deactivated the
595 + * plugin for every site (WordPress passes this
596 + * to deactivation hooks).
597 + */
598 + public static function deactivate( $network_wide = false ) {
468 599 // Drop-in + WP_CACHE constant are NOT touched here. WordPress
469 600 // upgrades run as deactivate → wipe files → install → activate,
470 601 // so removing those artifacts on every deactivate would silently
471 602 // disable caching after each plugin update. uninstall.php
@@ -472,9 +603,16 @@
472 603 // handles full teardown when the user actually removes the
473 604 // plugin; auto_heal() restores state on the next admin_init if
474 605 // the drop-in or WP_CACHE went missing for any other reason.
475 606 Cache::purge_all();
476 - Minifier::purge_minified();
607 + // purge_all() is site-scoped and no longer touches min/, so clear it
608 + // here. On a network where only this site is deactivating, the other
609 + // sites still link those files: drop this site's manifests only.
610 + if ( is_multisite() && ! $network_wide ) {
611 + Minifier::purge_manifests( Asset_Manifest::blog_id() );
612 + } else {
613 + Minifier::purge_minified();
614 + }
477 615 Gzip::apply( false );
478 616
479 617 Module_Registry::deactivate_all();
480 618 }