PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.1.2
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.1.2
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.2, at includes/class-plugin.php

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