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
xspeed / includes / class-plugin.php

class-plugin.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.4.2, at includes/class-plugin.php

620 lines 26.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Main plugin bootstrap.
4 *
5 * @package XSpeed
6 */
7
8 namespace XSpeed;
9
10 defined( 'ABSPATH' ) || exit;
11
12 class Plugin {
13
14 /**
15 * Data-schema version for one-time migrations, independent of the
16 * plugin version header. Bump when adding a step to maybe_upgrade().
17 */
18 public const DATA_VERSION = '1.3.6';
19
20 private static $instance = null;
21
22 /** @var Usage_Tracker|null */
23 private $usage_tracker = null;
24
25 public static function instance() {
26 if ( null === self::$instance ) {
27 self::$instance = new self();
28 }
29 return self::$instance;
30 }
31
32 public function init() {
33 // v1 services that are NOT yet wrapped as Modules (Admin, Rest_Api,
34 // Cache, Onboarding) are instantiated directly. Minifier is now
35 // instantiated by MinifyModule::boot(); Gzip is purely static.
36 new Admin();
37 new Rest_Api();
38 new Cache();
39 new Rest_Cache();
40 new Onboarding();
41
42 // Optional deactivation feedback survey on the Plugins screen. Admin
43 // context only (its hooks are admin_enqueue_scripts / admin_footer /
44 // wp_ajax). Sends nothing unless the user clicks "Submit & Deactivate".
45 if ( is_admin() ) {
46 new Deactivation_Feedback();
47 }
48
49 // Opt-in usage analytics. Instantiating + init() only registers the
50 // cron callback; NOTHING is collected or sent until the admin opts in
51 // from the setup wizard (Onboarding wires the consent toggle to
52 // Usage_Tracker::opt_in()). See class-usage-tracker.php privacy contract.
53 $this->start_plugin_tracking();
54
55 // Auto-heal the cache drop-in + WP_CACHE constant when state
56 // drifts. Scoped to admin_init only: filesystem writes belong in
57 // an authenticated admin context, never on anonymous front-end
58 // requests. The user already opted in (cache_enabled=true) —
59 // this is a consistency check, not a new install path. First
60 // admin page load after a plugin upgrade restores the state;
61 // front-end then serves from cache on the next request.
62 add_action( 'admin_init', array( Cache::class, 'auto_heal' ) );
63 // Cheap: returns immediately unless the stored schema version is
64 // behind. Covers updates and multisite, where activate() never runs.
65 add_action( 'admin_init', array( Score_Store::class, 'maybe_install' ) );
66
67 // Secondary net: restore as soon as an update completes, for the
68 // cases where activate() does not re-run (bulk updates, auto-updates,
69 // some host updaters). Best-effort by nature — this callback is only
70 // registered when we were loaded in the request performing the
71 // update, which is not guaranteed while WE are the plugin being
72 // replaced. The restore in activate() is the primary guarantee;
73 // auto_heal() on admin_init remains the backstop.
74 add_action( 'upgrader_process_complete', array( $this, 'maybe_restore_after_update' ), 10, 2 );
75
76 // Per-post cache rules (Phase 3.4) — registers postmeta with
77 // REST + meta box on edit screens.
78 Cache_Meta_Box::boot();
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
87 // Phase 0 architecture — managers + Free modules. v1 services
88 // (Cache/Minifier/Gzip) are NOT yet Modules; they'll be refactored
89 // in a follow-up PR with parity tests.
90 Conflict_Registry::boot();
91
92 // Read-only page-cache ownership evidence, behind the same
93 // activation/deactivation invalidation as the conflict matrix.
94 Page_Cache_Detector::boot();
95
96 // Integrations that clear OTHER plugins' caches of rendered output
97 // (Elementor's element cache + generated CSS). Registers a listener
98 // only; nothing runs until Cache::purge_render_caches() asks.
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();
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
122 // Register Free modules via the same action xspeed-pro uses, so
123 // the bootstrap path is symmetric across tiers.
124 add_action( 'xspeed_register_modules', array( $this, 'register_free_modules' ) );
125
126 // Fire the registration action + boot the registry at a LATER
127 // plugins_loaded priority so add-on plugins (xspeed-pro, in
128 // alphabetical order so it runs at default priority 10 AFTER
129 // us, but any add-on loaded at plugins_loaded(< 20)) have a
130 // chance to register their `xspeed_register_modules` callback
131 // before we fire the action.
132 //
133 // Bug history: previously this fired inline from init() at
134 // priority 10. xspeed-pro's plugins_loaded(15) hook then added
135 // its register_pro_modules callback AFTER the action had
136 // already fired — Pro modules never appeared in the registry.
137 // Caught by the ProStatus sentinel module's integration test.
138 add_action( 'plugins_loaded', array( $this, 'fire_module_lifecycle' ), 20 );
139
140 // One-time data migrations keyed on the stored version. Runs in
141 // admin only — nothing here needs to touch a front-end request.
142 if ( is_admin() ) {
143 add_action( 'plugins_loaded', array( $this, 'maybe_upgrade' ), 21 );
144 }
145 }
146
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 /**
178 * Run version-gated data migrations exactly once per upgrade.
179 *
180 * Keyed on `xspeed_data_version` rather than the plugin version header
181 * so a migration can be added without forcing a release bump.
182 */
183 public function maybe_upgrade(): void {
184 $current = (string) get_option( 'xspeed_data_version', '0' );
185 if ( version_compare( $current, self::DATA_VERSION, '>=' ) ) {
186 return;
187 }
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
193 // 1.1.2 — strip credential values recorded by earlier versions'
194 // settings change annotations (they're served by the trend endpoints).
195 if ( version_compare( $current, '1.1.2', '<' ) ) {
196 Activity_Log::redact_legacy_secrets();
197 }
198
199 // 1.1.4 — earlier versions cached a failed loopback as "gzip is not
200 // active" for an hour, which showed up as a bogus server-config
201 // warning. Drop the stale answer so the fixed probe re-runs instead
202 // of the wrong verdict living on past the update (issue #18).
203 if ( version_compare( $current, '1.1.4', '<' ) ) {
204 delete_transient( 'xspeed_gzip_active' );
205 }
206
207 // 1.1.6 — a `/?s=<term>` request used to write its results page into
208 // the static tree under the *searched-from* path, which for the usual
209 // query-form search is `/`. The web server then served that results
210 // page as the homepage to every visitor. The write is fixed in
211 // Cache::store_static(), but an entry poisoned before the update
212 // outlives it: nothing purges on upgrade, and the static serve path
213 // never revalidates. Clear the tree once. The flat cache is keyed
214 // correctly and is deliberately left alone. (issue #191)
215 if ( version_compare( $current, '1.1.6', '<' ) ) {
216 $removed = Cache::purge_static_tree();
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
257 update_option( 'xspeed_data_version', self::DATA_VERSION, false );
258 }
259
260 /**
261 * Phase 2 of plugin init: fire the registration action (collecting
262 * Free + Pro + any third-party modules hooked into
263 * `xspeed_register_modules`) and boot the registry.
264 *
265 * Runs at plugins_loaded(20) so every add-on that hooks at any
266 * priority < 20 has time to register first.
267 */
268 public function fire_module_lifecycle(): void {
269 $this->ensure_modules_registered();
270
271 Module_Registry::boot_all();
272 }
273
274 /**
275 * Fire `xspeed_register_modules` if this request has not yet, hooking
276 * Free's own registration first when init() never got the chance.
277 *
278 * The activation request is the case that matters. activate_plugin()
279 * includes the plugin file long after `plugins_loaded` has fired, so the
280 * `plugins_loaded` callback init() would have added never runs, and
281 * neither does the add_action() inside it that puts register_free_modules
282 * on the action. Firing the action from activate() then registered
283 * nothing: Settings::conflict_safe_profile() composed from an empty
284 * registry, and a site with WP Super Cache came up with lazy-load,
285 * resource hints, font swapping and preloading switched on — only the
286 * four settings the registry-independent fallback names were held down
287 * (PR #295 review). Module_Registry::activate_all() has been a no-op on
288 * the same request for the same reason.
289 *
290 * On the activation request that means Free only: an add-on cannot have
291 * hooked yet, because xspeed-pro bails when Free's classes are absent and
292 * only hooks the action (at priority 20, from plugins_loaded(15)) once
293 * Free is active. On an ordinary request fire_module_lifecycle() reaches
294 * this at plugins_loaded(20) with every add-on already hooked. Do not
295 * call this from anything that can run in between: the action fires
296 * once, and an add-on that has not hooked yet stays unregistered for the
297 * whole request.
298 * did_action() keeps the action to one firing per request, so an
299 * activation that ran first does not make plugins_loaded(20) register
300 * every module a second time.
301 */
302 public function ensure_modules_registered(): void {
303 if ( did_action( 'xspeed_register_modules' ) ) {
304 return;
305 }
306
307 /*
308 * register_free_modules() does an unconditional `new` on every module
309 * class. On an ordinary request xspeed.php's integrity check refuses
310 * to boot before that can fatal and explains itself in an admin
311 * notice; the activation hook is registered outside that check, so
312 * an install missing a module file (truncated zip, a security
313 * plugin's quarantine, a half-applied update) would fatal here with
314 * no notice and no active plugin. Same answer as boot: do nothing.
315 */
316 if ( function_exists( 'xspeed_missing_core_classes' ) && ! empty( xspeed_missing_core_classes() ) ) {
317 return;
318 }
319
320 if ( ! has_action( 'xspeed_register_modules', array( $this, 'register_free_modules' ) ) ) {
321 add_action( 'xspeed_register_modules', array( $this, 'register_free_modules' ) );
322 }
323
324 /**
325 * Action: xspeed_register_modules
326 *
327 * Free modules register at priority 10; xspeed-pro at priority
328 * 20; site code can hook in between to inject custom modules.
329 * Fires exactly once per request.
330 */
331 do_action( 'xspeed_register_modules' );
332 }
333
334 /**
335 * Register the Free Modules shipped in this plugin. Add new module
336 * registrations here. Pro plugin hooks the same action separately.
337 */
338 public function register_free_modules(): void {
339 Module_Registry::register( new \XSpeed\Modules\Cache\CacheModule() );
340 Module_Registry::register( new \XSpeed\Modules\Health\HealthModule() );
341 // Settings — owns no settings itself; it's the CLI/MCP surface over
342 // Settings_Manager. Registering it is what makes `xspeed settings`
343 // exist, which is what keeps the curated get_settings/update_settings
344 // tools in the MCP catalog. (#149/#153)
345 Module_Registry::register( new \XSpeed\Modules\Settings\SettingsModule() );
346 // External performance scores (PSI / GTmetrix) — Free, off by
347 // default. Rendered inside the Health host page's PageSpeed tab, so
348 // it has no sidebar row of its own.
349 Module_Registry::register( new \XSpeed\Modules\Score\ScoreModule() );
350 Module_Registry::register( new \XSpeed\Modules\Preloader\PreloaderModule() );
351 Module_Registry::register( new \XSpeed\Modules\Heartbeat\HeartbeatModule() );
352 Module_Registry::register( new \XSpeed\Modules\Minify\MinifyModule() );
353 Module_Registry::register( new \XSpeed\Modules\Gzip\GzipModule() );
354 Module_Registry::register( new \XSpeed\Modules\Lazy\LazyModule() );
355 Module_Registry::register( new \XSpeed\Modules\Bloat\BloatModule() );
356 Module_Registry::register( new \XSpeed\Modules\Database\DatabaseModule() );
357 Module_Registry::register( new \XSpeed\Modules\Cdn\CdnModule() );
358 Module_Registry::register( new \XSpeed\Modules\Cloudflare\CloudflareModule() );
359 Module_Registry::register( new \XSpeed\Modules\ObjectCache\ObjectCacheModule() );
360 Module_Registry::register( new \XSpeed\Modules\BrowserCache\BrowserCacheModule() );
361 // Advanced Cache — a Free container row that gathers the Pro
362 // cache-coverage features (404 / search / feed / REST / rules /
363 // maintenance) into one sidebar sub-item (FBS-83633).
364 Module_Registry::register( new \XSpeed\Modules\CacheCoverage\CacheCoverageModule() );
365 Module_Registry::register( new \XSpeed\Modules\Fonts\FontsModule() );
366 Module_Registry::register( new \XSpeed\Modules\TurboRender\TurboRenderModule() );
367 Module_Registry::register( new \XSpeed\Modules\ResourceHints\ResourceHintsModule() );
368 // AI Privacy (GDPR off-switch) ships in Free even though every AI
369 // *feature* is Pro — privacy is a right, not a paid tier. FEATURES.md
370 // §AI row 6 mandates it. Without this registration the module was dead
371 // code: no REST/settings surface, the promised off-switch unreachable
372 // (FBS-83633 Bug 1). It carries its own cli_commands() so it satisfies
373 // the CLI/MCP coverage guard once registered.
374 Module_Registry::register( new \XSpeed\Modules\AIPrivacy\AIPrivacyModule() );
375 // Migration moved Pro → Free: it's an acquisition/onboarding feature
376 // (detect a competing caching plugin, import its settings, switch over),
377 // so it must work without a Pro license. Agency-scale extras (profiles,
378 // bulk multisite, host presets) remain Pro.
379 Module_Registry::register( new \XSpeed\Modules\Migration\MigrationModule() );
380 // Help & Support moved Pro → Free: a ticket link + read-only system
381 // snapshot is onboarding/diagnostics, not a paid value-add, so every
382 // user gets it. The snapshot degrades gracefully without Pro (Pro
383 // version/license fields fall back to defaults via defined()/get_option).
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() );
389 // MCP remote control (AI assistants) — Free. The plugin serves the
390 // MCP protocol at the site's own /xspeed/mcp URL; the only gate is
391 // the per-site connection token an admin mints via Connect. No
392 // license, no hosted infra. See IMPLEMENTATION.md §17.
393 Module_Registry::register( new \XSpeed\Modules\Mcp\McpModule() );
394 }
395
396 /**
397 * Boot the opt-in usage tracker. Registers the cron sender only; the send
398 * itself is consent-gated inside Usage_Tracker. The instance is held so the
399 * onboarding REST handler can flip consent via usage_tracker()->opt_in().
400 */
401 public function start_plugin_tracking(): void {
402 $this->usage_tracker = Usage_Tracker::get_instance(
403 XSPEED_FILE,
404 array(
405 'opt_in' => true,
406 'email_marketing' => true,
407 'item_id' => defined( 'XSPEED_INSIGHTS_ITEM_ID' ) ? XSPEED_INSIGHTS_ITEM_ID : false,
408 )
409 );
410 $this->usage_tracker->init();
411 }
412
413 /**
414 * The shared Usage_Tracker singleton (or null if tracking wasn't booted,
415 * e.g. on the activation hook before init() runs).
416 */
417 public function usage_tracker(): ?Usage_Tracker {
418 return $this->usage_tracker;
419 }
420
421 public static function activate() {
422 // Nothing below can see a module the registry does not hold — the
423 // conflict-safe profile Settings::set_defaults() may pick is composed
424 // from it, and activate_all() walks it. See ensure_modules_registered()
425 // for why the registry is empty on this request without this call.
426 self::instance()->ensure_modules_registered();
427
428 $profile = Settings::set_defaults();
429
430 // Score history table. Also called on admin_init (see init()) because
431 // activation does not fire for a site added to a multisite network
432 // later, nor after an update that ships a new schema version.
433 Score_Store::maybe_install();
434
435 if ( ! file_exists( XSPEED_CACHE_DIR ) ) {
436 wp_mkdir_p( XSPEED_CACHE_DIR );
437 }
438 Cache::write_silence( XSPEED_CACHE_DIR );
439
440 /*
441 * One exception to "caching is only ever enabled from the admin UI":
442 * a fresh install another plugin performed on the user's behalf.
443 *
444 * That plugin asked the user for site performance and installed us to
445 * provide it; making them go and find a second switch afterwards is
446 * a step nobody wants. It is also what the copy-vendored Setup::finish()
447 * did, so hosts migrating off it keep the behaviour they have.
448 *
449 * PROFILE_HOST_PAGE_CACHE is the whole condition, and it means three
450 * things at once: the install was genuinely fresh, nothing else owns
451 * the page cache, and another plugin claimed the install. Every other
452 * fresh install — including a user's own on a clear site — waits for
453 * the wizard. Every OTHER feature is off in this profile; the cache is
454 * the one thing a host may assume, because it is what it installed us
455 * for. And toggle() runs its own ownership transaction, so a
456 * competitor appearing between the two checks loses the race rather
457 * than being overwritten.
458 */
459 if ( Settings::PROFILE_HOST_PAGE_CACHE === $profile ) {
460 $state = Cache::toggle( true );
461 if ( ! empty( $state['blocked'] ) ) {
462 /*
463 * Refused after all — a competitor that appeared between the
464 * profile decision and the write, or a drop-in we could not
465 * install. The settings are identical either way (everything
466 * off), so the honest record is the one that does not claim a
467 * cache: conflict-safe is what the site actually got.
468 */
469 $profile = Settings::PROFILE_CONFLICT_SAFE;
470 update_option( 'xspeed_install_profile', $profile, false );
471 }
472 }
473
474 /*
475 * Read the claim BEFORE spending it. consume_installed_by() records
476 * the installer only for a FRESH install — on a re-activation over
477 * settings that are already there it deletes the arming option and
478 * keeps nothing — so asking again afterwards answered "the user did
479 * it" about an install a host had just claimed, and the wizard opened
480 * over the host's own flow. The claim decides who to tell and whether
481 * to open the wizard; whether it is worth RECORDING is a separate
482 * question, and only the recording depends on the install being fresh.
483 */
484 $installed_by = Settings::installed_by();
485
486 // Spend the arming option now the profile is settled. It changes what
487 // activation does, so it may not survive into the next one.
488 Settings::consume_installed_by( $profile );
489
490 // Caching is otherwise only ENABLED from the admin UI — see
491 // Cache::toggle() and Rest_Api::toggle_cache(). A fresh install
492 // therefore gets no drop-in and no wp-config.php edit here:
493 // cache_enabled is unset, so the call below is a no-op.
494 //
495 // It is NOT a no-op during an upgrade. WordPress runs an update as
496 // deactivate → wipe files → install → activate, which deletes
497 // advanced-cache.php while cache_enabled stays true. Restoring it
498 // here closes the window in which the site silently serves uncached
499 // (auto_heal() alone only fires on the next wp-admin page load).
500 Cache::restore_dropin_if_enabled();
501
502 /*
503 * First-run wizard: flag a one-time redirect for the activating user.
504 * Suppressed for bulk activations / already-completed sites in
505 * Onboarding::maybe_redirect() — and here for an install another plugin
506 * performed, which has an onboarding flow of its own. The install runs
507 * over AJAX, so our redirect would fire on that admin's NEXT page load
508 * and pull them out of the middle of the host's wizard; finishing ours
509 * would then overwrite the deliberately all-off profile they never
510 * asked to change.
511 */
512 if ( '' === $installed_by ) {
513 Onboarding::flag_redirect();
514 }
515
516 // Propagate activation to every registered Module — registered by
517 // ensure_modules_registered() at the top, not by plugins_loaded,
518 // which fired before this file was even included.
519 Module_Registry::activate_all();
520
521 /**
522 * Fires at the end of activation, once the settings profile is decided.
523 *
524 * The other half of the host-plugin contract: a plugin that installed
525 * xSpeed for the user writes `xspeed_installed_by` before activating
526 * and listens here to find out how it came up. It carries no return
527 * value and nothing branches on it — a host that ignores it changes
528 * nothing about the install.
529 *
530 * @param string $installed_by Host slug, or '' when the user did it.
531 * @param string $profile Settings::PROFILE_* — which profile a fresh
532 * install came up with, '' if not fresh.
533 */
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' );
549 }
550
551 /**
552 * Restore the cache drop-in right after THIS plugin is updated.
553 *
554 * Bound to upgrader_process_complete. Bulk updates, auto-updates and
555 * host-level updaters finish without re-running activate(), so this is
556 * the only hook that repairs the drop-in before the next wp-admin page
557 * load. Narrow by design: bails unless the completed action was a
558 * plugin update whose payload actually includes xspeed.
559 *
560 * @param \WP_Upgrader $upgrader Upgrader instance (unused).
561 * @param array $hook_extra Contextual data about the update.
562 * @return void
563 */
564 public function maybe_restore_after_update( $upgrader, $hook_extra ) {
565 unset( $upgrader );
566
567 if ( ! is_array( $hook_extra ) ) {
568 return;
569 }
570 if ( ! isset( $hook_extra['type'], $hook_extra['action'] ) ) {
571 return;
572 }
573 if ( 'plugin' !== $hook_extra['type'] || 'update' !== $hook_extra['action'] ) {
574 return;
575 }
576
577 // Single update uses 'plugin'; bulk uses 'plugins'.
578 $updated = array();
579 if ( isset( $hook_extra['plugins'] ) && is_array( $hook_extra['plugins'] ) ) {
580 $updated = $hook_extra['plugins'];
581 } elseif ( isset( $hook_extra['plugin'] ) && is_string( $hook_extra['plugin'] ) ) {
582 $updated = array( $hook_extra['plugin'] );
583 }
584
585 $ours = plugin_basename( XSPEED_FILE );
586 if ( ! in_array( $ours, $updated, true ) ) {
587 return;
588 }
589
590 Cache::restore_dropin_if_enabled();
591 }
592
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 ) {
599 // Drop-in + WP_CACHE constant are NOT touched here. WordPress
600 // upgrades run as deactivate → wipe files → install → activate,
601 // so removing those artifacts on every deactivate would silently
602 // disable caching after each plugin update. uninstall.php
603 // handles full teardown when the user actually removes the
604 // plugin; auto_heal() restores state on the next admin_init if
605 // the drop-in or WP_CACHE went missing for any other reason.
606 Cache::purge_all();
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 }
615 Gzip::apply( false );
616
617 Module_Registry::deactivate_all();
618 }
619 }
620