PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.1.3
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.1.3
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 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
xspeed / includes / class-plugin.php

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

310 lines 12.5 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.1.2';
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
64 // Secondary net: restore as soon as an update completes, for the
65 // cases where activate() does not re-run (bulk updates, auto-updates,
66 // some host updaters). Best-effort by nature — this callback is only
67 // registered when we were loaded in the request performing the
68 // update, which is not guaranteed while WE are the plugin being
69 // replaced. The restore in activate() is the primary guarantee;
70 // auto_heal() on admin_init remains the backstop.
71 add_action( 'upgrader_process_complete', array( $this, 'maybe_restore_after_update' ), 10, 2 );
72
73 // Per-post cache rules (Phase 3.4) — registers postmeta with
74 // REST + meta box on edit screens.
75 Cache_Meta_Box::boot();
76
77 // Phase 0 architecture — managers + Free modules. v1 services
78 // (Cache/Minifier/Gzip) are NOT yet Modules; they'll be refactored
79 // in a follow-up PR with parity tests.
80 Conflict_Registry::boot();
81
82 // Register Free modules via the same action xspeed-pro uses, so
83 // the bootstrap path is symmetric across tiers.
84 add_action( 'xspeed_register_modules', array( $this, 'register_free_modules' ) );
85
86 // Fire the registration action + boot the registry at a LATER
87 // plugins_loaded priority so add-on plugins (xspeed-pro, in
88 // alphabetical order so it runs at default priority 10 AFTER
89 // us, but any add-on loaded at plugins_loaded(< 20)) have a
90 // chance to register their `xspeed_register_modules` callback
91 // before we fire the action.
92 //
93 // Bug history: previously this fired inline from init() at
94 // priority 10. xspeed-pro's plugins_loaded(15) hook then added
95 // its register_pro_modules callback AFTER the action had
96 // already fired — Pro modules never appeared in the registry.
97 // Caught by the ProStatus sentinel module's integration test.
98 add_action( 'plugins_loaded', array( $this, 'fire_module_lifecycle' ), 20 );
99
100 // One-time data migrations keyed on the stored version. Runs in
101 // admin only — nothing here needs to touch a front-end request.
102 if ( is_admin() ) {
103 add_action( 'plugins_loaded', array( $this, 'maybe_upgrade' ), 21 );
104 }
105 }
106
107 /**
108 * Run version-gated data migrations exactly once per upgrade.
109 *
110 * Keyed on `xspeed_data_version` rather than the plugin version header
111 * so a migration can be added without forcing a release bump.
112 */
113 public function maybe_upgrade(): void {
114 $current = (string) get_option( 'xspeed_data_version', '0' );
115 if ( version_compare( $current, self::DATA_VERSION, '>=' ) ) {
116 return;
117 }
118
119 // 1.1.2 — strip credential values recorded by earlier versions'
120 // settings change annotations (they're served by the trend endpoints).
121 Activity_Log::redact_legacy_secrets();
122
123 update_option( 'xspeed_data_version', self::DATA_VERSION, false );
124 }
125
126 /**
127 * Phase 2 of plugin init: fire the registration action (collecting
128 * Free + Pro + any third-party modules hooked into
129 * `xspeed_register_modules`) and boot the registry.
130 *
131 * Runs at plugins_loaded(20) so every add-on that hooks at any
132 * priority < 20 has time to register first.
133 */
134 public function fire_module_lifecycle(): void {
135 /**
136 * Action: xspeed_register_modules
137 *
138 * Free modules register at priority 10; xspeed-pro at priority
139 * 20; site code can hook in between to inject custom modules.
140 * Fires exactly once per request.
141 */
142 do_action( 'xspeed_register_modules' );
143
144 Module_Registry::boot_all();
145 }
146
147 /**
148 * Register the Free Modules shipped in this plugin. Add new module
149 * registrations here. Pro plugin hooks the same action separately.
150 */
151 public function register_free_modules(): void {
152 Module_Registry::register( new \XSpeed\Modules\Cache\CacheModule() );
153 Module_Registry::register( new \XSpeed\Modules\Health\HealthModule() );
154 // External performance scores (PSI / GTmetrix) — Free, off by
155 // default. Rendered inside the Health host page's PageSpeed tab, so
156 // it has no sidebar row of its own.
157 Module_Registry::register( new \XSpeed\Modules\Score\ScoreModule() );
158 Module_Registry::register( new \XSpeed\Modules\Preloader\PreloaderModule() );
159 Module_Registry::register( new \XSpeed\Modules\Heartbeat\HeartbeatModule() );
160 Module_Registry::register( new \XSpeed\Modules\Minify\MinifyModule() );
161 Module_Registry::register( new \XSpeed\Modules\Gzip\GzipModule() );
162 Module_Registry::register( new \XSpeed\Modules\Lazy\LazyModule() );
163 Module_Registry::register( new \XSpeed\Modules\Bloat\BloatModule() );
164 Module_Registry::register( new \XSpeed\Modules\Database\DatabaseModule() );
165 Module_Registry::register( new \XSpeed\Modules\Cdn\CdnModule() );
166 Module_Registry::register( new \XSpeed\Modules\Cloudflare\CloudflareModule() );
167 Module_Registry::register( new \XSpeed\Modules\ObjectCache\ObjectCacheModule() );
168 Module_Registry::register( new \XSpeed\Modules\BrowserCache\BrowserCacheModule() );
169 // Advanced Cache — a Free container row that gathers the Pro
170 // cache-coverage features (404 / search / feed / REST / rules /
171 // maintenance) into one sidebar sub-item (FBS-83633).
172 Module_Registry::register( new \XSpeed\Modules\CacheCoverage\CacheCoverageModule() );
173 Module_Registry::register( new \XSpeed\Modules\Fonts\FontsModule() );
174 Module_Registry::register( new \XSpeed\Modules\ResourceHints\ResourceHintsModule() );
175 // AI Privacy (GDPR off-switch) ships in Free even though every AI
176 // *feature* is Pro — privacy is a right, not a paid tier. FEATURES.md
177 // §AI row 6 mandates it. Without this registration the module was dead
178 // code: no REST/settings surface, the promised off-switch unreachable
179 // (FBS-83633 Bug 1). It carries its own cli_commands() so it satisfies
180 // the CLI/MCP coverage guard once registered.
181 Module_Registry::register( new \XSpeed\Modules\AIPrivacy\AIPrivacyModule() );
182 // Migration moved Pro → Free: it's an acquisition/onboarding feature
183 // (detect a competing caching plugin, import its settings, switch over),
184 // so it must work without a Pro license. Agency-scale extras (profiles,
185 // bulk multisite, host presets) remain Pro.
186 Module_Registry::register( new \XSpeed\Modules\Migration\MigrationModule() );
187 // Help & Support moved Pro → Free: a ticket link + read-only system
188 // snapshot is onboarding/diagnostics, not a paid value-add, so every
189 // user gets it. The snapshot degrades gracefully without Pro (Pro
190 // version/license fields fall back to defaults via defined()/get_option).
191 Module_Registry::register( new \XSpeed\Modules\Support\SupportModule() );
192 // MCP remote control (AI assistants) — Free. The plugin serves the
193 // MCP protocol at the site's own /xspeed/mcp URL; the only gate is
194 // the per-site connection token an admin mints via Connect. No
195 // license, no hosted infra. See IMPLEMENTATION.md §17.
196 Module_Registry::register( new \XSpeed\Modules\Mcp\McpModule() );
197 }
198
199 /**
200 * Boot the opt-in usage tracker. Registers the cron sender only; the send
201 * itself is consent-gated inside Usage_Tracker. The instance is held so the
202 * onboarding REST handler can flip consent via usage_tracker()->opt_in().
203 */
204 public function start_plugin_tracking(): void {
205 $this->usage_tracker = Usage_Tracker::get_instance(
206 XSPEED_FILE,
207 array(
208 'opt_in' => true,
209 'item_id' => defined( 'XSPEED_INSIGHTS_ITEM_ID' ) ? XSPEED_INSIGHTS_ITEM_ID : false,
210 )
211 );
212 $this->usage_tracker->init();
213 }
214
215 /**
216 * The shared Usage_Tracker singleton (or null if tracking wasn't booted,
217 * e.g. on the activation hook before init() runs).
218 */
219 public function usage_tracker(): ?Usage_Tracker {
220 return $this->usage_tracker;
221 }
222
223 public static function activate() {
224 Settings::set_defaults();
225
226 if ( ! file_exists( XSPEED_CACHE_DIR ) ) {
227 wp_mkdir_p( XSPEED_CACHE_DIR );
228 }
229 Cache::write_silence( XSPEED_CACHE_DIR );
230
231 // Caching is only ever ENABLED from the admin UI — see Cache::toggle()
232 // and Rest_Api::toggle_cache(). A fresh install therefore gets no
233 // drop-in and no wp-config.php edit here: cache_enabled is unset, so
234 // the call below is a no-op.
235 //
236 // It is NOT a no-op during an upgrade. WordPress runs an update as
237 // deactivate → wipe files → install → activate, which deletes
238 // advanced-cache.php while cache_enabled stays true. Restoring it
239 // here closes the window in which the site silently serves uncached
240 // (auto_heal() alone only fires on the next wp-admin page load).
241 Cache::restore_dropin_if_enabled();
242
243 // First-run wizard: flag a one-time redirect for the activating user.
244 // Suppressed for bulk activations / already-completed sites in
245 // Onboarding::maybe_redirect().
246 Onboarding::flag_redirect();
247
248 // Propagate activation to every registered Module. Activation
249 // happens after plugins_loaded → modules are already registered.
250 Module_Registry::activate_all();
251 }
252
253 /**
254 * Restore the cache drop-in right after THIS plugin is updated.
255 *
256 * Bound to upgrader_process_complete. Bulk updates, auto-updates and
257 * host-level updaters finish without re-running activate(), so this is
258 * the only hook that repairs the drop-in before the next wp-admin page
259 * load. Narrow by design: bails unless the completed action was a
260 * plugin update whose payload actually includes xspeed.
261 *
262 * @param \WP_Upgrader $upgrader Upgrader instance (unused).
263 * @param array $hook_extra Contextual data about the update.
264 * @return void
265 */
266 public function maybe_restore_after_update( $upgrader, $hook_extra ) {
267 unset( $upgrader );
268
269 if ( ! is_array( $hook_extra ) ) {
270 return;
271 }
272 if ( ! isset( $hook_extra['type'], $hook_extra['action'] ) ) {
273 return;
274 }
275 if ( 'plugin' !== $hook_extra['type'] || 'update' !== $hook_extra['action'] ) {
276 return;
277 }
278
279 // Single update uses 'plugin'; bulk uses 'plugins'.
280 $updated = array();
281 if ( isset( $hook_extra['plugins'] ) && is_array( $hook_extra['plugins'] ) ) {
282 $updated = $hook_extra['plugins'];
283 } elseif ( isset( $hook_extra['plugin'] ) && is_string( $hook_extra['plugin'] ) ) {
284 $updated = array( $hook_extra['plugin'] );
285 }
286
287 $ours = plugin_basename( XSPEED_FILE );
288 if ( ! in_array( $ours, $updated, true ) ) {
289 return;
290 }
291
292 Cache::restore_dropin_if_enabled();
293 }
294
295 public static function deactivate() {
296 // Drop-in + WP_CACHE constant are NOT touched here. WordPress
297 // upgrades run as deactivate → wipe files → install → activate,
298 // so removing those artifacts on every deactivate would silently
299 // disable caching after each plugin update. uninstall.php
300 // handles full teardown when the user actually removes the
301 // plugin; auto_heal() restores state on the next admin_init if
302 // the drop-in or WP_CACHE went missing for any other reason.
303 Cache::purge_all();
304 Minifier::purge_minified();
305 Gzip::apply( false );
306
307 Module_Registry::deactivate_all();
308 }
309 }
310