start_plugin_tracking(); // Auto-heal the cache drop-in + WP_CACHE constant when state // drifts. Scoped to admin_init only: filesystem writes belong in // an authenticated admin context, never on anonymous front-end // requests. The user already opted in (cache_enabled=true) — // this is a consistency check, not a new install path. First // admin page load after a plugin upgrade restores the state; // front-end then serves from cache on the next request. add_action( 'admin_init', array( Cache::class, 'auto_heal' ) ); // Per-post cache rules (Phase 3.4) — registers postmeta with // REST + meta box on edit screens. Cache_Meta_Box::boot(); // Phase 0 architecture — managers + Free modules. v1 services // (Cache/Minifier/Gzip) are NOT yet Modules; they'll be refactored // in a follow-up PR with parity tests. Conflict_Registry::boot(); // Register Free modules via the same action xspeed-pro uses, so // the bootstrap path is symmetric across tiers. add_action( 'xspeed_register_modules', array( $this, 'register_free_modules' ) ); // Fire the registration action + boot the registry at a LATER // plugins_loaded priority so add-on plugins (xspeed-pro, in // alphabetical order so it runs at default priority 10 AFTER // us, but any add-on loaded at plugins_loaded(< 20)) have a // chance to register their `xspeed_register_modules` callback // before we fire the action. // // Bug history: previously this fired inline from init() at // priority 10. xspeed-pro's plugins_loaded(15) hook then added // its register_pro_modules callback AFTER the action had // already fired — Pro modules never appeared in the registry. // Caught by the ProStatus sentinel module's integration test. add_action( 'plugins_loaded', array( $this, 'fire_module_lifecycle' ), 20 ); // One-time data migrations keyed on the stored version. Runs in // admin only — nothing here needs to touch a front-end request. if ( is_admin() ) { add_action( 'plugins_loaded', array( $this, 'maybe_upgrade' ), 21 ); } } /** * Run version-gated data migrations exactly once per upgrade. * * Keyed on `xspeed_data_version` rather than the plugin version header * so a migration can be added without forcing a release bump. */ public function maybe_upgrade(): void { $current = (string) get_option( 'xspeed_data_version', '0' ); if ( version_compare( $current, self::DATA_VERSION, '>=' ) ) { return; } // 1.1.2 — strip credential values recorded by earlier versions' // settings change annotations (they're served by the trend endpoints). Activity_Log::redact_legacy_secrets(); update_option( 'xspeed_data_version', self::DATA_VERSION, false ); } /** * Phase 2 of plugin init: fire the registration action (collecting * Free + Pro + any third-party modules hooked into * `xspeed_register_modules`) and boot the registry. * * Runs at plugins_loaded(20) so every add-on that hooks at any * priority < 20 has time to register first. */ public function fire_module_lifecycle(): void { /** * Action: xspeed_register_modules * * Free modules register at priority 10; xspeed-pro at priority * 20; site code can hook in between to inject custom modules. * Fires exactly once per request. */ do_action( 'xspeed_register_modules' ); Module_Registry::boot_all(); } /** * Register the Free Modules shipped in this plugin. Add new module * registrations here. Pro plugin hooks the same action separately. */ public function register_free_modules(): void { Module_Registry::register( new \XSpeed\Modules\Cache\CacheModule() ); Module_Registry::register( new \XSpeed\Modules\Health\HealthModule() ); // External performance scores (PSI / GTmetrix) — Free, off by // default. Rendered inside the Health host page's PageSpeed tab, so // it has no sidebar row of its own. Module_Registry::register( new \XSpeed\Modules\Score\ScoreModule() ); Module_Registry::register( new \XSpeed\Modules\Preloader\PreloaderModule() ); Module_Registry::register( new \XSpeed\Modules\Heartbeat\HeartbeatModule() ); Module_Registry::register( new \XSpeed\Modules\Minify\MinifyModule() ); Module_Registry::register( new \XSpeed\Modules\Gzip\GzipModule() ); Module_Registry::register( new \XSpeed\Modules\Lazy\LazyModule() ); Module_Registry::register( new \XSpeed\Modules\Bloat\BloatModule() ); Module_Registry::register( new \XSpeed\Modules\Database\DatabaseModule() ); Module_Registry::register( new \XSpeed\Modules\Cdn\CdnModule() ); Module_Registry::register( new \XSpeed\Modules\Cloudflare\CloudflareModule() ); Module_Registry::register( new \XSpeed\Modules\ObjectCache\ObjectCacheModule() ); Module_Registry::register( new \XSpeed\Modules\BrowserCache\BrowserCacheModule() ); // Advanced Cache — a Free container row that gathers the Pro // cache-coverage features (404 / search / feed / REST / rules / // maintenance) into one sidebar sub-item (FBS-83633). Module_Registry::register( new \XSpeed\Modules\CacheCoverage\CacheCoverageModule() ); Module_Registry::register( new \XSpeed\Modules\Fonts\FontsModule() ); Module_Registry::register( new \XSpeed\Modules\ResourceHints\ResourceHintsModule() ); // AI Privacy (GDPR off-switch) ships in Free even though every AI // *feature* is Pro — privacy is a right, not a paid tier. FEATURES.md // §AI row 6 mandates it. Without this registration the module was dead // code: no REST/settings surface, the promised off-switch unreachable // (FBS-83633 Bug 1). It carries its own cli_commands() so it satisfies // the CLI/MCP coverage guard once registered. Module_Registry::register( new \XSpeed\Modules\AIPrivacy\AIPrivacyModule() ); // Migration moved Pro → Free: it's an acquisition/onboarding feature // (detect a competing caching plugin, import its settings, switch over), // so it must work without a Pro license. Agency-scale extras (profiles, // bulk multisite, host presets) remain Pro. Module_Registry::register( new \XSpeed\Modules\Migration\MigrationModule() ); // Help & Support moved Pro → Free: a ticket link + read-only system // snapshot is onboarding/diagnostics, not a paid value-add, so every // user gets it. The snapshot degrades gracefully without Pro (Pro // version/license fields fall back to defaults via defined()/get_option). Module_Registry::register( new \XSpeed\Modules\Support\SupportModule() ); // MCP remote control (AI assistants) — Free. The plugin serves the // MCP protocol at the site's own /xspeed/mcp URL; the only gate is // the per-site connection token an admin mints via Connect. No // license, no hosted infra. See IMPLEMENTATION.md §17. Module_Registry::register( new \XSpeed\Modules\Mcp\McpModule() ); // Settings host page (FBS-83633): a Free container whose tabs embed // AI Provider / AI Privacy / MCP / License / Branding / Multisite. // Folds the old AI + Pro Add-ons sidebar groups into one Settings // destination and gives the Free AI-Privacy off-switch a stable home. Module_Registry::register( new \XSpeed\Modules\Settings\SettingsModule() ); } /** * Boot the opt-in usage tracker. Registers the cron sender only; the send * itself is consent-gated inside Usage_Tracker. The instance is held so the * onboarding REST handler can flip consent via usage_tracker()->opt_in(). */ public function start_plugin_tracking(): void { $this->usage_tracker = Usage_Tracker::get_instance( XSPEED_FILE, array( 'opt_in' => true, 'item_id' => defined( 'XSPEED_INSIGHTS_ITEM_ID' ) ? XSPEED_INSIGHTS_ITEM_ID : false, ) ); $this->usage_tracker->init(); } /** * The shared Usage_Tracker singleton (or null if tracking wasn't booted, * e.g. on the activation hook before init() runs). */ public function usage_tracker(): ?Usage_Tracker { return $this->usage_tracker; } public static function activate() { Settings::set_defaults(); if ( ! file_exists( XSPEED_CACHE_DIR ) ) { wp_mkdir_p( XSPEED_CACHE_DIR ); } Cache::write_silence( XSPEED_CACHE_DIR ); // Drop-in installation (advanced-cache.php) and the WP_CACHE constant // edit happen only when the user explicitly enables caching from the // admin UI — never on activation. See Cache::toggle() and // Rest_Api::toggle_cache(). This is a WordPress.org review requirement. // First-run wizard: flag a one-time redirect for the activating user. // Suppressed for bulk activations / already-completed sites in // Onboarding::maybe_redirect(). Onboarding::flag_redirect(); // Propagate activation to every registered Module. Activation // happens after plugins_loaded → modules are already registered. Module_Registry::activate_all(); } public static function deactivate() { // Drop-in + WP_CACHE constant are NOT touched here. WordPress // upgrades run as deactivate → wipe files → install → activate, // so removing those artifacts on every deactivate would silently // disable caching after each plugin update. uninstall.php // handles full teardown when the user actually removes the // plugin; auto_heal() restores state on the next admin_init if // the drop-in or WP_CACHE went missing for any other reason. Cache::purge_all(); Minifier::purge_minified(); Gzip::apply( false ); Module_Registry::deactivate_all(); } }