| @@ -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,23 @@ | ||
| 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 | + // Tell WP Statistics and Slimstat not to count the warmer, the | |
| 108 | + // benchmark and the verifier as visitors. Record-time filters only; | |
| 109 | + // see the class for why the tracking snippet itself is left alone. | |
| 110 | + Self_Traffic::boot(); | |
| 111 | + | |
| 94 | 112 | // Register Free modules via the same action xspeed-pro uses, so |
| 95 | 113 | // the bootstrap path is symmetric across tiers. |
| 96 | 114 | add_action( 'xspeed_register_modules', array( $this, 'register_free_modules' ) ); |
| 97 | 115 | |
| @@ -116,8 +134,38 @@ | ||
| 116 | 134 | } |
| 117 | 135 | } |
| 118 | 136 | |
| 119 | 137 | /** |
| 138 | + * Add the 1.3.6 tracking params to a saved ignored-params list. | |
| 139 | + * | |
| 140 | + * Only those names, and only when missing: anything else the site | |
| 141 | + * removed stays removed. A site with no saved list already reads the new | |
| 142 | + * defaults. | |
| 143 | + */ | |
| 144 | + public static function add_new_ignored_params(): void { | |
| 145 | + $option = Settings_Manager::OPTION_PREFIX . 'cache'; | |
| 146 | + $stored = get_option( $option, array() ); | |
| 147 | + if ( ! is_array( $stored ) || ! is_array( $stored['ignored_query_params'] ?? null ) ) { | |
| 148 | + return; | |
| 149 | + } | |
| 150 | + $list = $stored['ignored_query_params']; | |
| 151 | + // An empty list is a choice: every query string bypasses the cache. | |
| 152 | + if ( empty( $list ) ) { | |
| 153 | + return; | |
| 154 | + } | |
| 155 | + $missing = array_values( array_diff( \XSpeed\Modules\Cache\CacheModule::TRACKING_PARAMS_1_3_6, $list ) ); | |
| 156 | + if ( empty( $missing ) ) { | |
| 157 | + return; | |
| 158 | + } | |
| 159 | + $stored['ignored_query_params'] = array_merge( $list, $missing ); | |
| 160 | + update_option( $option, $stored ); | |
| 161 | + // The drop-in keeps its own copy of the list next to the cache. | |
| 162 | + if ( defined( 'XSPEED_CACHE_DIR' ) ) { | |
| 163 | + Cache::sync_query_allowlist(); | |
| 164 | + } | |
| 165 | + } | |
| 166 | + | |
| 167 | + /** | |
| 120 | 168 | * Run version-gated data migrations exactly once per upgrade. |
| 121 | 169 | * |
| 122 | 170 | * Keyed on `xspeed_data_version` rather than the plugin version header |
| 123 | 171 | * so a migration can be added without forcing a release bump. |
| @@ -127,17 +175,25 @@ | ||
| 127 | 175 | if ( version_compare( $current, self::DATA_VERSION, '>=' ) ) { |
| 128 | 176 | return; |
| 129 | 177 | } |
| 130 | 178 | |
| 179 | + // Each step runs once, for the version that needs it. They used to | |
| 180 | + // run on every bump, which would purge every site's static tree again | |
| 181 | + // for a migration that has nothing to do with it. | |
| 182 | + | |
| 131 | 183 | // 1.1.2 — strip credential values recorded by earlier versions' |
| 132 | 184 | // settings change annotations (they're served by the trend endpoints). |
| 133 | - Activity_Log::redact_legacy_secrets(); | |
| 185 | + if ( version_compare( $current, '1.1.2', '<' ) ) { | |
| 186 | + Activity_Log::redact_legacy_secrets(); | |
| 187 | + } | |
| 134 | 188 | |
| 135 | 189 | // 1.1.4 — earlier versions cached a failed loopback as "gzip is not |
| 136 | 190 | // active" for an hour, which showed up as a bogus server-config |
| 137 | 191 | // warning. Drop the stale answer so the fixed probe re-runs instead |
| 138 | 192 | // of the wrong verdict living on past the update (issue #18). |
| 139 | - delete_transient( 'xspeed_gzip_active' ); | |
| 193 | + if ( version_compare( $current, '1.1.4', '<' ) ) { | |
| 194 | + delete_transient( 'xspeed_gzip_active' ); | |
| 195 | + } | |
| 140 | 196 | |
| 141 | 197 | // 1.1.6 — a `/?s=<term>` request used to write its results page into |
| 142 | 198 | // the static tree under the *searched-from* path, which for the usual |
| 143 | 199 | // query-form search is `/`. The web server then served that results |
| @@ -145,10 +201,19 @@ | ||
| 145 | 201 | // Cache::store_static(), but an entry poisoned before the update |
| 146 | 202 | // outlives it: nothing purges on upgrade, and the static serve path |
| 147 | 203 | // never revalidates. Clear the tree once. The flat cache is keyed |
| 148 | 204 | // correctly and is deliberately left alone. (issue #191) |
| 149 | - Cache::purge_static_tree(); | |
| 205 | + if ( version_compare( $current, '1.1.6', '<' ) ) { | |
| 206 | + Cache::purge_static_tree(); | |
| 207 | + } | |
| 150 | 208 | |
| 209 | + // 1.3.6 — new click and campaign IDs in the default ignored list. | |
| 210 | + // A site that ever saved the Cache panel has its own copy of the list, | |
| 211 | + // which the new defaults never reach, so add them to it. | |
| 212 | + if ( version_compare( $current, '1.3.6', '<' ) ) { | |
| 213 | + self::add_new_ignored_params(); | |
| 214 | + } | |
| 215 | + | |
| 151 | 216 | update_option( 'xspeed_data_version', self::DATA_VERSION, false ); |
| 152 | 217 | } |
| 153 | 218 | |
| 154 | 219 | /** |
| @@ -256,8 +321,9 @@ | ||
| 256 | 321 | // cache-coverage features (404 / search / feed / REST / rules / |
| 257 | 322 | // maintenance) into one sidebar sub-item (FBS-83633). |
| 258 | 323 | Module_Registry::register( new \XSpeed\Modules\CacheCoverage\CacheCoverageModule() ); |
| 259 | 324 | Module_Registry::register( new \XSpeed\Modules\Fonts\FontsModule() ); |
| 325 | + Module_Registry::register( new \XSpeed\Modules\TurboRender\TurboRenderModule() ); | |
| 260 | 326 | Module_Registry::register( new \XSpeed\Modules\ResourceHints\ResourceHintsModule() ); |
| 261 | 327 | // AI Privacy (GDPR off-switch) ships in Free even though every AI |
| 262 | 328 | // *feature* is Pro — privacy is a right, not a paid tier. FEATURES.md |
| 263 | 329 | // §AI row 6 mandates it. Without this registration the module was dead |
| @@ -274,8 +340,12 @@ | ||
| 274 | 340 | // snapshot is onboarding/diagnostics, not a paid value-add, so every |
| 275 | 341 | // user gets it. The snapshot degrades gracefully without Pro (Pro |
| 276 | 342 | // version/license fields fall back to defaults via defined()/get_option). |
| 277 | 343 | Module_Registry::register( new \XSpeed\Modules\Support\SupportModule() ); |
| 344 | + // The dashboard control for usage-analytics consent. Consent used to | |
| 345 | + // be collectable only in the wizard and withdrawable nowhere, while | |
| 346 | + // the wizard and readme both promised a dashboard switch. (#437) | |
| 347 | + Module_Registry::register( new \XSpeed\Modules\Privacy\PrivacyModule() ); | |
| 278 | 348 | // MCP remote control (AI assistants) — Free. The plugin serves the |
| 279 | 349 | // MCP protocol at the site's own /xspeed/mcp URL; the only gate is |
| 280 | 350 | // the per-site connection token an admin mints via Connect. No |
| 281 | 351 | // license, no hosted infra. See IMPLEMENTATION.md §17. |
| @@ -290,10 +360,11 @@ | ||
| 290 | 360 | public function start_plugin_tracking(): void { |
| 291 | 361 | $this->usage_tracker = Usage_Tracker::get_instance( |
| 292 | 362 | XSPEED_FILE, |
| 293 | 363 | array( |
| 294 | - 'opt_in' => true, | |
| 295 | - 'item_id' => defined( 'XSPEED_INSIGHTS_ITEM_ID' ) ? XSPEED_INSIGHTS_ITEM_ID : false, | |
| 364 | + 'opt_in' => true, | |
| 365 | + 'email_marketing' => true, | |
| 366 | + 'item_id' => defined( 'XSPEED_INSIGHTS_ITEM_ID' ) ? XSPEED_INSIGHTS_ITEM_ID : false, | |
| 296 | 367 | ) |
| 297 | 368 | ); |
| 298 | 369 | $this->usage_tracker->init(); |
| 299 | 370 | } |