| 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.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 |
// Phase 0 architecture — managers + Free modules. v1 services |
| 81 |
// (Cache/Minifier/Gzip) are NOT yet Modules; they'll be refactored |
| 82 |
// in a follow-up PR with parity tests. |
| 83 |
Conflict_Registry::boot(); |
| 84 |
|
| 85 |
// Read-only page-cache ownership evidence, behind the same |
| 86 |
// activation/deactivation invalidation as the conflict matrix. |
| 87 |
Page_Cache_Detector::boot(); |
| 88 |
|
| 89 |
// Integrations that clear OTHER plugins' caches of rendered output |
| 90 |
// (Elementor's element cache + generated CSS). Registers a listener |
| 91 |
// only; nothing runs until Cache::purge_render_caches() asks. |
| 92 |
Render_Caches::boot(); |
| 93 |
|
| 94 |
// Register Free modules via the same action xspeed-pro uses, so |
| 95 |
// the bootstrap path is symmetric across tiers. |
| 96 |
add_action( 'xspeed_register_modules', array( $this, 'register_free_modules' ) ); |
| 97 |
|
| 98 |
// Fire the registration action + boot the registry at a LATER |
| 99 |
// plugins_loaded priority so add-on plugins (xspeed-pro, in |
| 100 |
// alphabetical order so it runs at default priority 10 AFTER |
| 101 |
// us, but any add-on loaded at plugins_loaded(< 20)) have a |
| 102 |
// chance to register their `xspeed_register_modules` callback |
| 103 |
// before we fire the action. |
| 104 |
// |
| 105 |
// Bug history: previously this fired inline from init() at |
| 106 |
// priority 10. xspeed-pro's plugins_loaded(15) hook then added |
| 107 |
// its register_pro_modules callback AFTER the action had |
| 108 |
// already fired — Pro modules never appeared in the registry. |
| 109 |
// Caught by the ProStatus sentinel module's integration test. |
| 110 |
add_action( 'plugins_loaded', array( $this, 'fire_module_lifecycle' ), 20 ); |
| 111 |
|
| 112 |
// One-time data migrations keyed on the stored version. Runs in |
| 113 |
// admin only — nothing here needs to touch a front-end request. |
| 114 |
if ( is_admin() ) { |
| 115 |
add_action( 'plugins_loaded', array( $this, 'maybe_upgrade' ), 21 ); |
| 116 |
} |
| 117 |
} |
| 118 |
|
| 119 |
/** |
| 120 |
* Run version-gated data migrations exactly once per upgrade. |
| 121 |
* |
| 122 |
* Keyed on `xspeed_data_version` rather than the plugin version header |
| 123 |
* so a migration can be added without forcing a release bump. |
| 124 |
*/ |
| 125 |
public function maybe_upgrade(): void { |
| 126 |
$current = (string) get_option( 'xspeed_data_version', '0' ); |
| 127 |
if ( version_compare( $current, self::DATA_VERSION, '>=' ) ) { |
| 128 |
return; |
| 129 |
} |
| 130 |
|
| 131 |
// 1.1.2 — strip credential values recorded by earlier versions' |
| 132 |
// settings change annotations (they're served by the trend endpoints). |
| 133 |
Activity_Log::redact_legacy_secrets(); |
| 134 |
|
| 135 |
// 1.1.4 — earlier versions cached a failed loopback as "gzip is not |
| 136 |
// active" for an hour, which showed up as a bogus server-config |
| 137 |
// warning. Drop the stale answer so the fixed probe re-runs instead |
| 138 |
// of the wrong verdict living on past the update (issue #18). |
| 139 |
delete_transient( 'xspeed_gzip_active' ); |
| 140 |
|
| 141 |
// 1.1.6 — a `/?s=<term>` request used to write its results page into |
| 142 |
// the static tree under the *searched-from* path, which for the usual |
| 143 |
// query-form search is `/`. The web server then served that results |
| 144 |
// page as the homepage to every visitor. The write is fixed in |
| 145 |
// Cache::store_static(), but an entry poisoned before the update |
| 146 |
// outlives it: nothing purges on upgrade, and the static serve path |
| 147 |
// never revalidates. Clear the tree once. The flat cache is keyed |
| 148 |
// correctly and is deliberately left alone. (issue #191) |
| 149 |
Cache::purge_static_tree(); |
| 150 |
|
| 151 |
update_option( 'xspeed_data_version', self::DATA_VERSION, false ); |
| 152 |
} |
| 153 |
|
| 154 |
/** |
| 155 |
* Phase 2 of plugin init: fire the registration action (collecting |
| 156 |
* Free + Pro + any third-party modules hooked into |
| 157 |
* `xspeed_register_modules`) and boot the registry. |
| 158 |
* |
| 159 |
* Runs at plugins_loaded(20) so every add-on that hooks at any |
| 160 |
* priority < 20 has time to register first. |
| 161 |
*/ |
| 162 |
public function fire_module_lifecycle(): void { |
| 163 |
$this->ensure_modules_registered(); |
| 164 |
|
| 165 |
Module_Registry::boot_all(); |
| 166 |
} |
| 167 |
|
| 168 |
/** |
| 169 |
* Fire `xspeed_register_modules` if this request has not yet, hooking |
| 170 |
* Free's own registration first when init() never got the chance. |
| 171 |
* |
| 172 |
* The activation request is the case that matters. activate_plugin() |
| 173 |
* includes the plugin file long after `plugins_loaded` has fired, so the |
| 174 |
* `plugins_loaded` callback init() would have added never runs, and |
| 175 |
* neither does the add_action() inside it that puts register_free_modules |
| 176 |
* on the action. Firing the action from activate() then registered |
| 177 |
* nothing: Settings::conflict_safe_profile() composed from an empty |
| 178 |
* registry, and a site with WP Super Cache came up with lazy-load, |
| 179 |
* resource hints, font swapping and preloading switched on — only the |
| 180 |
* four settings the registry-independent fallback names were held down |
| 181 |
* (PR #295 review). Module_Registry::activate_all() has been a no-op on |
| 182 |
* the same request for the same reason. |
| 183 |
* |
| 184 |
* On the activation request that means Free only: an add-on cannot have |
| 185 |
* hooked yet, because xspeed-pro bails when Free's classes are absent and |
| 186 |
* only hooks the action (at priority 20, from plugins_loaded(15)) once |
| 187 |
* Free is active. On an ordinary request fire_module_lifecycle() reaches |
| 188 |
* this at plugins_loaded(20) with every add-on already hooked. Do not |
| 189 |
* call this from anything that can run in between: the action fires |
| 190 |
* once, and an add-on that has not hooked yet stays unregistered for the |
| 191 |
* whole request. |
| 192 |
* did_action() keeps the action to one firing per request, so an |
| 193 |
* activation that ran first does not make plugins_loaded(20) register |
| 194 |
* every module a second time. |
| 195 |
*/ |
| 196 |
public function ensure_modules_registered(): void { |
| 197 |
if ( did_action( 'xspeed_register_modules' ) ) { |
| 198 |
return; |
| 199 |
} |
| 200 |
|
| 201 |
/* |
| 202 |
* register_free_modules() does an unconditional `new` on every module |
| 203 |
* class. On an ordinary request xspeed.php's integrity check refuses |
| 204 |
* to boot before that can fatal and explains itself in an admin |
| 205 |
* notice; the activation hook is registered outside that check, so |
| 206 |
* an install missing a module file (truncated zip, a security |
| 207 |
* plugin's quarantine, a half-applied update) would fatal here with |
| 208 |
* no notice and no active plugin. Same answer as boot: do nothing. |
| 209 |
*/ |
| 210 |
if ( function_exists( 'xspeed_missing_core_classes' ) && ! empty( xspeed_missing_core_classes() ) ) { |
| 211 |
return; |
| 212 |
} |
| 213 |
|
| 214 |
if ( ! has_action( 'xspeed_register_modules', array( $this, 'register_free_modules' ) ) ) { |
| 215 |
add_action( 'xspeed_register_modules', array( $this, 'register_free_modules' ) ); |
| 216 |
} |
| 217 |
|
| 218 |
/** |
| 219 |
* Action: xspeed_register_modules |
| 220 |
* |
| 221 |
* Free modules register at priority 10; xspeed-pro at priority |
| 222 |
* 20; site code can hook in between to inject custom modules. |
| 223 |
* Fires exactly once per request. |
| 224 |
*/ |
| 225 |
do_action( 'xspeed_register_modules' ); |
| 226 |
} |
| 227 |
|
| 228 |
/** |
| 229 |
* Register the Free Modules shipped in this plugin. Add new module |
| 230 |
* registrations here. Pro plugin hooks the same action separately. |
| 231 |
*/ |
| 232 |
public function register_free_modules(): void { |
| 233 |
Module_Registry::register( new \XSpeed\Modules\Cache\CacheModule() ); |
| 234 |
Module_Registry::register( new \XSpeed\Modules\Health\HealthModule() ); |
| 235 |
// Settings — owns no settings itself; it's the CLI/MCP surface over |
| 236 |
// Settings_Manager. Registering it is what makes `xspeed settings` |
| 237 |
// exist, which is what keeps the curated get_settings/update_settings |
| 238 |
// tools in the MCP catalog. (#149/#153) |
| 239 |
Module_Registry::register( new \XSpeed\Modules\Settings\SettingsModule() ); |
| 240 |
// External performance scores (PSI / GTmetrix) — Free, off by |
| 241 |
// default. Rendered inside the Health host page's PageSpeed tab, so |
| 242 |
// it has no sidebar row of its own. |
| 243 |
Module_Registry::register( new \XSpeed\Modules\Score\ScoreModule() ); |
| 244 |
Module_Registry::register( new \XSpeed\Modules\Preloader\PreloaderModule() ); |
| 245 |
Module_Registry::register( new \XSpeed\Modules\Heartbeat\HeartbeatModule() ); |
| 246 |
Module_Registry::register( new \XSpeed\Modules\Minify\MinifyModule() ); |
| 247 |
Module_Registry::register( new \XSpeed\Modules\Gzip\GzipModule() ); |
| 248 |
Module_Registry::register( new \XSpeed\Modules\Lazy\LazyModule() ); |
| 249 |
Module_Registry::register( new \XSpeed\Modules\Bloat\BloatModule() ); |
| 250 |
Module_Registry::register( new \XSpeed\Modules\Database\DatabaseModule() ); |
| 251 |
Module_Registry::register( new \XSpeed\Modules\Cdn\CdnModule() ); |
| 252 |
Module_Registry::register( new \XSpeed\Modules\Cloudflare\CloudflareModule() ); |
| 253 |
Module_Registry::register( new \XSpeed\Modules\ObjectCache\ObjectCacheModule() ); |
| 254 |
Module_Registry::register( new \XSpeed\Modules\BrowserCache\BrowserCacheModule() ); |
| 255 |
// Advanced Cache — a Free container row that gathers the Pro |
| 256 |
// cache-coverage features (404 / search / feed / REST / rules / |
| 257 |
// maintenance) into one sidebar sub-item (FBS-83633). |
| 258 |
Module_Registry::register( new \XSpeed\Modules\CacheCoverage\CacheCoverageModule() ); |
| 259 |
Module_Registry::register( new \XSpeed\Modules\Fonts\FontsModule() ); |
| 260 |
Module_Registry::register( new \XSpeed\Modules\ResourceHints\ResourceHintsModule() ); |
| 261 |
// AI Privacy (GDPR off-switch) ships in Free even though every AI |
| 262 |
// *feature* is Pro — privacy is a right, not a paid tier. FEATURES.md |
| 263 |
// §AI row 6 mandates it. Without this registration the module was dead |
| 264 |
// code: no REST/settings surface, the promised off-switch unreachable |
| 265 |
// (FBS-83633 Bug 1). It carries its own cli_commands() so it satisfies |
| 266 |
// the CLI/MCP coverage guard once registered. |
| 267 |
Module_Registry::register( new \XSpeed\Modules\AIPrivacy\AIPrivacyModule() ); |
| 268 |
// Migration moved Pro → Free: it's an acquisition/onboarding feature |
| 269 |
// (detect a competing caching plugin, import its settings, switch over), |
| 270 |
// so it must work without a Pro license. Agency-scale extras (profiles, |
| 271 |
// bulk multisite, host presets) remain Pro. |
| 272 |
Module_Registry::register( new \XSpeed\Modules\Migration\MigrationModule() ); |
| 273 |
// Help & Support moved Pro → Free: a ticket link + read-only system |
| 274 |
// snapshot is onboarding/diagnostics, not a paid value-add, so every |
| 275 |
// user gets it. The snapshot degrades gracefully without Pro (Pro |
| 276 |
// version/license fields fall back to defaults via defined()/get_option). |
| 277 |
Module_Registry::register( new \XSpeed\Modules\Support\SupportModule() ); |
| 278 |
// MCP remote control (AI assistants) — Free. The plugin serves the |
| 279 |
// MCP protocol at the site's own /xspeed/mcp URL; the only gate is |
| 280 |
// the per-site connection token an admin mints via Connect. No |
| 281 |
// license, no hosted infra. See IMPLEMENTATION.md §17. |
| 282 |
Module_Registry::register( new \XSpeed\Modules\Mcp\McpModule() ); |
| 283 |
} |
| 284 |
|
| 285 |
/** |
| 286 |
* Boot the opt-in usage tracker. Registers the cron sender only; the send |
| 287 |
* itself is consent-gated inside Usage_Tracker. The instance is held so the |
| 288 |
* onboarding REST handler can flip consent via usage_tracker()->opt_in(). |
| 289 |
*/ |
| 290 |
public function start_plugin_tracking(): void { |
| 291 |
$this->usage_tracker = Usage_Tracker::get_instance( |
| 292 |
XSPEED_FILE, |
| 293 |
array( |
| 294 |
'opt_in' => true, |
| 295 |
'item_id' => defined( 'XSPEED_INSIGHTS_ITEM_ID' ) ? XSPEED_INSIGHTS_ITEM_ID : false, |
| 296 |
) |
| 297 |
); |
| 298 |
$this->usage_tracker->init(); |
| 299 |
} |
| 300 |
|
| 301 |
/** |
| 302 |
* The shared Usage_Tracker singleton (or null if tracking wasn't booted, |
| 303 |
* e.g. on the activation hook before init() runs). |
| 304 |
*/ |
| 305 |
public function usage_tracker(): ?Usage_Tracker { |
| 306 |
return $this->usage_tracker; |
| 307 |
} |
| 308 |
|
| 309 |
public static function activate() { |
| 310 |
// Nothing below can see a module the registry does not hold — the |
| 311 |
// conflict-safe profile Settings::set_defaults() may pick is composed |
| 312 |
// from it, and activate_all() walks it. See ensure_modules_registered() |
| 313 |
// for why the registry is empty on this request without this call. |
| 314 |
self::instance()->ensure_modules_registered(); |
| 315 |
|
| 316 |
$profile = Settings::set_defaults(); |
| 317 |
|
| 318 |
// Score history table. Also called on admin_init (see init()) because |
| 319 |
// activation does not fire for a site added to a multisite network |
| 320 |
// later, nor after an update that ships a new schema version. |
| 321 |
Score_Store::maybe_install(); |
| 322 |
|
| 323 |
if ( ! file_exists( XSPEED_CACHE_DIR ) ) { |
| 324 |
wp_mkdir_p( XSPEED_CACHE_DIR ); |
| 325 |
} |
| 326 |
Cache::write_silence( XSPEED_CACHE_DIR ); |
| 327 |
|
| 328 |
/* |
| 329 |
* One exception to "caching is only ever enabled from the admin UI": |
| 330 |
* a fresh install another plugin performed on the user's behalf. |
| 331 |
* |
| 332 |
* That plugin asked the user for site performance and installed us to |
| 333 |
* provide it; making them go and find a second switch afterwards is |
| 334 |
* a step nobody wants. It is also what the copy-vendored Setup::finish() |
| 335 |
* did, so hosts migrating off it keep the behaviour they have. |
| 336 |
* |
| 337 |
* PROFILE_HOST_PAGE_CACHE is the whole condition, and it means three |
| 338 |
* things at once: the install was genuinely fresh, nothing else owns |
| 339 |
* the page cache, and another plugin claimed the install. Every other |
| 340 |
* fresh install — including a user's own on a clear site — waits for |
| 341 |
* the wizard. Every OTHER feature is off in this profile; the cache is |
| 342 |
* the one thing a host may assume, because it is what it installed us |
| 343 |
* for. And toggle() runs its own ownership transaction, so a |
| 344 |
* competitor appearing between the two checks loses the race rather |
| 345 |
* than being overwritten. |
| 346 |
*/ |
| 347 |
if ( Settings::PROFILE_HOST_PAGE_CACHE === $profile ) { |
| 348 |
$state = Cache::toggle( true ); |
| 349 |
if ( ! empty( $state['blocked'] ) ) { |
| 350 |
/* |
| 351 |
* Refused after all — a competitor that appeared between the |
| 352 |
* profile decision and the write, or a drop-in we could not |
| 353 |
* install. The settings are identical either way (everything |
| 354 |
* off), so the honest record is the one that does not claim a |
| 355 |
* cache: conflict-safe is what the site actually got. |
| 356 |
*/ |
| 357 |
$profile = Settings::PROFILE_CONFLICT_SAFE; |
| 358 |
update_option( 'xspeed_install_profile', $profile, false ); |
| 359 |
} |
| 360 |
} |
| 361 |
|
| 362 |
/* |
| 363 |
* Read the claim BEFORE spending it. consume_installed_by() records |
| 364 |
* the installer only for a FRESH install — on a re-activation over |
| 365 |
* settings that are already there it deletes the arming option and |
| 366 |
* keeps nothing — so asking again afterwards answered "the user did |
| 367 |
* it" about an install a host had just claimed, and the wizard opened |
| 368 |
* over the host's own flow. The claim decides who to tell and whether |
| 369 |
* to open the wizard; whether it is worth RECORDING is a separate |
| 370 |
* question, and only the recording depends on the install being fresh. |
| 371 |
*/ |
| 372 |
$installed_by = Settings::installed_by(); |
| 373 |
|
| 374 |
// Spend the arming option now the profile is settled. It changes what |
| 375 |
// activation does, so it may not survive into the next one. |
| 376 |
Settings::consume_installed_by( $profile ); |
| 377 |
|
| 378 |
// Caching is otherwise only ENABLED from the admin UI — see |
| 379 |
// Cache::toggle() and Rest_Api::toggle_cache(). A fresh install |
| 380 |
// therefore gets no drop-in and no wp-config.php edit here: |
| 381 |
// cache_enabled is unset, so the call below is a no-op. |
| 382 |
// |
| 383 |
// It is NOT a no-op during an upgrade. WordPress runs an update as |
| 384 |
// deactivate → wipe files → install → activate, which deletes |
| 385 |
// advanced-cache.php while cache_enabled stays true. Restoring it |
| 386 |
// here closes the window in which the site silently serves uncached |
| 387 |
// (auto_heal() alone only fires on the next wp-admin page load). |
| 388 |
Cache::restore_dropin_if_enabled(); |
| 389 |
|
| 390 |
/* |
| 391 |
* First-run wizard: flag a one-time redirect for the activating user. |
| 392 |
* Suppressed for bulk activations / already-completed sites in |
| 393 |
* Onboarding::maybe_redirect() — and here for an install another plugin |
| 394 |
* performed, which has an onboarding flow of its own. The install runs |
| 395 |
* over AJAX, so our redirect would fire on that admin's NEXT page load |
| 396 |
* and pull them out of the middle of the host's wizard; finishing ours |
| 397 |
* would then overwrite the deliberately all-off profile they never |
| 398 |
* asked to change. |
| 399 |
*/ |
| 400 |
if ( '' === $installed_by ) { |
| 401 |
Onboarding::flag_redirect(); |
| 402 |
} |
| 403 |
|
| 404 |
// Propagate activation to every registered Module — registered by |
| 405 |
// ensure_modules_registered() at the top, not by plugins_loaded, |
| 406 |
// which fired before this file was even included. |
| 407 |
Module_Registry::activate_all(); |
| 408 |
|
| 409 |
/** |
| 410 |
* Fires at the end of activation, once the settings profile is decided. |
| 411 |
* |
| 412 |
* The other half of the host-plugin contract: a plugin that installed |
| 413 |
* xSpeed for the user writes `xspeed_installed_by` before activating |
| 414 |
* and listens here to find out how it came up. It carries no return |
| 415 |
* value and nothing branches on it — a host that ignores it changes |
| 416 |
* nothing about the install. |
| 417 |
* |
| 418 |
* @param string $installed_by Host slug, or '' when the user did it. |
| 419 |
* @param string $profile Settings::PROFILE_* — which profile a fresh |
| 420 |
* install came up with, '' if not fresh. |
| 421 |
*/ |
| 422 |
do_action( 'xspeed_activated', $installed_by, $profile ); |
| 423 |
} |
| 424 |
|
| 425 |
/** |
| 426 |
* Restore the cache drop-in right after THIS plugin is updated. |
| 427 |
* |
| 428 |
* Bound to upgrader_process_complete. Bulk updates, auto-updates and |
| 429 |
* host-level updaters finish without re-running activate(), so this is |
| 430 |
* the only hook that repairs the drop-in before the next wp-admin page |
| 431 |
* load. Narrow by design: bails unless the completed action was a |
| 432 |
* plugin update whose payload actually includes xspeed. |
| 433 |
* |
| 434 |
* @param \WP_Upgrader $upgrader Upgrader instance (unused). |
| 435 |
* @param array $hook_extra Contextual data about the update. |
| 436 |
* @return void |
| 437 |
*/ |
| 438 |
public function maybe_restore_after_update( $upgrader, $hook_extra ) { |
| 439 |
unset( $upgrader ); |
| 440 |
|
| 441 |
if ( ! is_array( $hook_extra ) ) { |
| 442 |
return; |
| 443 |
} |
| 444 |
if ( ! isset( $hook_extra['type'], $hook_extra['action'] ) ) { |
| 445 |
return; |
| 446 |
} |
| 447 |
if ( 'plugin' !== $hook_extra['type'] || 'update' !== $hook_extra['action'] ) { |
| 448 |
return; |
| 449 |
} |
| 450 |
|
| 451 |
// Single update uses 'plugin'; bulk uses 'plugins'. |
| 452 |
$updated = array(); |
| 453 |
if ( isset( $hook_extra['plugins'] ) && is_array( $hook_extra['plugins'] ) ) { |
| 454 |
$updated = $hook_extra['plugins']; |
| 455 |
} elseif ( isset( $hook_extra['plugin'] ) && is_string( $hook_extra['plugin'] ) ) { |
| 456 |
$updated = array( $hook_extra['plugin'] ); |
| 457 |
} |
| 458 |
|
| 459 |
$ours = plugin_basename( XSPEED_FILE ); |
| 460 |
if ( ! in_array( $ours, $updated, true ) ) { |
| 461 |
return; |
| 462 |
} |
| 463 |
|
| 464 |
Cache::restore_dropin_if_enabled(); |
| 465 |
} |
| 466 |
|
| 467 |
public static function deactivate() { |
| 468 |
// Drop-in + WP_CACHE constant are NOT touched here. WordPress |
| 469 |
// upgrades run as deactivate → wipe files → install → activate, |
| 470 |
// so removing those artifacts on every deactivate would silently |
| 471 |
// disable caching after each plugin update. uninstall.php |
| 472 |
// handles full teardown when the user actually removes the |
| 473 |
// plugin; auto_heal() restores state on the next admin_init if |
| 474 |
// the drop-in or WP_CACHE went missing for any other reason. |
| 475 |
Cache::purge_all(); |
| 476 |
Minifier::purge_minified(); |
| 477 |
Gzip::apply( false ); |
| 478 |
|
| 479 |
Module_Registry::deactivate_all(); |
| 480 |
} |
| 481 |
} |
| 482 |
|