PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.1
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.1
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 1.2.0 All 28 releases
xspeed / uninstall.php

uninstall.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.3.1, at uninstall.php

205 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Uninstall handler — deletes options, cache, and drop-in.
4 *
5 * @package XSpeed
6 */
7
8 if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
9 exit;
10 }
11
12 xspeed_uninstall_cleanup();
13
14 /**
15 * Run all uninstall cleanup. Wrapped in a function so locals don't pollute global scope.
16 */
17 function xspeed_uninstall_cleanup() {
18 /*
19 * Everything here is ours to delete. `wpdeveloper_xspeed_offer` is NOT — it
20 * is the site's answer about whether it wants this plugin, written by
21 * whichever WPDeveloper plugin offered it. Removing xSpeed is itself an
22 * answer — the siblings read it as one, from that row plus the absent plugin
23 * files. Deleting it here would make every one of them offer xSpeed again to
24 * the user who just took it off.
25 * See docs/guides/installing-from-another-plugin.md.
26 */
27 delete_option( 'xspeed_options' );
28
29 /*
30 * Per-module rows, for the modules THIS plugin ships. The slugs are read
31 * out of the module files rather than kept as a list here, so a module
32 * added later cannot leave its row behind; and only Free's, because
33 * xspeed-pro keeps its own rows under the same prefix and a Free uninstall
34 * is not a decision about Pro's settings.
35 *
36 * These have to go. The conflict-safe profile writes an explicit `false`
37 * into every one of them when another plugin owns the page cache, and
38 * seeding on the next install only fills ABSENT keys — so a row that
39 * survived uninstall kept every switch off on a site that had since been
40 * cleared, with no way back but the dashboard (PR #295 review).
41 */
42 foreach ( glob( __DIR__ . '/includes/modules/*/*Module.php' ) ?: array() as $module_file ) {
43 $source = @file_get_contents( $module_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- own plugin file, uninstall.
44 if ( is_string( $source ) && preg_match( "/const\s+SLUG\s*=\s*'([^']+)'/", $source, $m ) ) {
45 delete_option( 'xspeed_module_' . $m[1] );
46 }
47 }
48 delete_option( 'xspeed_data_version' );
49 delete_option( 'xspeed_activity_log' );
50 delete_option( 'xspeed_server_type' );
51 delete_option( 'xspeed_oc_dropin_synced' );
52 delete_option( 'xspeed_oc_generation' );
53 delete_option( 'xspeed_oc_sync_attempts' );
54 delete_option( 'xspeed_overridden_constants' );
55 delete_option( 'xspeed_last_mobile_separate' );
56 delete_option( 'xspeed_redirect_to_onboarding' );
57 delete_option( 'xspeed_onboarding_complete' );
58 // Provenance a host plugin wrote before it activated us, and the profile
59 // that install came up with.
60 delete_option( 'xspeed_installed_by' );
61 delete_option( 'xspeed_installer' );
62 delete_option( 'xspeed_install_profile' );
63 // Written by the copy-vendored Setup that host plugins used to carry
64 // before xSpeed seeded its own conflict-safe profile on activation. We no
65 // longer write it; an older host may have, and it is ours to clean up.
66 delete_option( 'xspeed_setup_snapshot' );
67 delete_option( 'xspeed_stats' );
68 delete_option( 'xspeed_gc_cursor' );
69 wp_clear_scheduled_hook( 'xspeed_gc' );
70
71 // Score history — the plugin's own table, plus the legacy option the
72 // table was migrated from (kept on upgrade so a bad migration is
73 // recoverable; there is nothing to recover on uninstall).
74 global $wpdb;
75 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- own table, uninstall.
76 $wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'xspeed_scores' );
77 delete_option( 'xspeed_score_schema' );
78 delete_option( 'xspeed_score_history' );
79
80 if ( ! function_exists( 'WP_Filesystem' ) ) {
81 require_once ABSPATH . 'wp-admin/includes/file.php';
82 }
83 WP_Filesystem();
84 global $wp_filesystem;
85 if ( ! $wp_filesystem ) {
86 return;
87 }
88
89 $cache_dir = WP_CONTENT_DIR . '/cache/xspeed';
90 if ( $wp_filesystem->is_dir( $cache_dir ) ) {
91 $wp_filesystem->delete( $cache_dir, true );
92 }
93
94 // nginx hit log (FBS-82478). It lives under uploads/xspeed/, NOT the
95 // cache dir, precisely so that a pasted nginx `access_log` directive
96 // pointing at it does NOT get its parent directory deleted here — if it
97 // did, `nginx -t` would fail [emerg] and refuse to (re)start, taking
98 // down EVERY vhost on the host until someone manually finds the orphaned
99 // directive. We therefore EMPTY the log file but DELIBERATELY LEAVE THE
100 // DIRECTORY in place, so any still-pasted directive keeps a valid,
101 // openable target after the plugin is gone. (A stray empty dir is
102 // harmless; a broken nginx is not.) Users are also warned in the admin
103 // UI to remove the snippet before uninstalling.
104 $hits_log = WP_CONTENT_DIR . '/uploads/xspeed/hits.log';
105 if ( function_exists( 'wp_upload_dir' ) ) {
106 $uploads = wp_upload_dir( null, false );
107 if ( is_array( $uploads ) && empty( $uploads['error'] ) && ! empty( $uploads['basedir'] ) ) {
108 $hits_log = rtrim( (string) $uploads['basedir'], '/' ) . '/xspeed/hits.log';
109 }
110 }
111 if ( $wp_filesystem->exists( $hits_log ) ) {
112 // Truncate, don't delete the dir — keep the access_log target openable.
113 $wp_filesystem->put_contents( $hits_log, '', FS_CHMOD_FILE );
114 }
115
116 // Static-cache tree — separate from the flat-hash cache dir, holds
117 // the {host}/{path}/index.html files the .htaccess rewrite block
118 // serves directly. Same teardown rules as XSPEED_CACHE_DIR.
119 $static_dir = WP_CONTENT_DIR . '/cache/xspeed-static';
120 if ( $wp_filesystem->is_dir( $static_dir ) ) {
121 $wp_filesystem->delete( $static_dir, true );
122 }
123
124 // Rewrite block in site-root .htaccess. WP's marker helpers handle
125 // the "remove only our block" semantics — passing an empty array
126 // strips the markers in place.
127 $htaccess = ABSPATH . '.htaccess';
128 if ( $wp_filesystem->exists( $htaccess ) && $wp_filesystem->is_writable( $htaccess ) ) {
129 if ( ! function_exists( 'insert_with_markers' ) ) {
130 require_once ABSPATH . 'wp-admin/includes/misc.php';
131 }
132 insert_with_markers( $htaccess, 'xSpeed Static Cache', array() );
133 }
134
135 // Serialize the shared drop-in/config teardown with Cache::toggle(). If
136 // the lock is unavailable, leave shared state and its receipt untouched.
137 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen,WordPress.PHP.NoSilencedErrors.Discouraged -- flock requires a local handle; failure is fail-closed.
138 $ownership_lock = @fopen( WP_CONTENT_DIR . '/.xspeed-page-cache.lock', 'c+' );
139 if ( ! is_resource( $ownership_lock ) || ! flock( $ownership_lock, LOCK_EX ) ) {
140 return;
141 }
142
143 require_once __DIR__ . '/includes/wp-cache-constant.php';
144 $drop_in = WP_CONTENT_DIR . '/advanced-cache.php';
145 $dropin_ours = false;
146 if ( $wp_filesystem->exists( $drop_in ) ) {
147 $contents = $wp_filesystem->get_contents( $drop_in );
148 $dropin_ours = is_string( $contents ) && xspeed_has_canonical_dropin_signature( $contents );
149 if ( $dropin_ours ) {
150 $wp_filesystem->delete( $drop_in );
151 }
152 }
153
154 $wp_config = file_exists( ABSPATH . 'wp-config.php' ) ? ABSPATH . 'wp-config.php' : dirname( ABSPATH ) . '/wp-config.php';
155 // `defined()` alone, not `&& WP_CACHE`: the old truthiness test skipped
156 // the removal whenever the constant was false/0 — exactly the orphan we
157 // are here to clean up — while still entering it for TRUE/1, where the
158 // hardcoded-lowercase regex then matched nothing and reported success.
159 // Strip whatever spelling is there. (#9)
160 //
161 // Gated on the drop-in being ours: if another caching plugin holds
162 // advanced-cache.php, WP_CACHE is the switch that loads THEIR file, and
163 // stripping it on our way out would silently disable their page cache.
164 if ( $wp_filesystem->is_writable( $wp_config ) ) {
165 $config = $wp_filesystem->get_contents( $wp_config );
166 if ( is_string( $config ) ) {
167 // Same helper Cache::set_wp_cache_constant() uses — one pattern,
168 // one place. uninstall.php loads no plugin classes, hence a
169 // plain requirable function rather than a method.
170 require_once __DIR__ . '/includes/wp-cache-constant.php';
171 $receipt = get_option( 'xspeed_page_cache_ownership_receipt', '' );
172 $marked = xspeed_wp_cache_receipt_matches( $config, $receipt );
173 /*
174 * A receipt proves WE wrote the line; it does not prove the line
175 * is still ours to remove. If a competitor has since taken over
176 * advanced-cache.php, WP_CACHE is what loads THEIR drop-in — they
177 * had no reason to touch an already-true define, so our receipt
178 * comment is still sitting beside it. Stripping on the receipt
179 * alone silently stopped their live page cache.
180 *
181 * A foreign drop-in therefore vetoes the removal outright, which
182 * is the same rule Cache::set_wp_cache_constant() applies in the
183 * running plugin; only this path was missing it. `$dropin_ours`
184 * covers the file we just deleted, `$marked` the case where there
185 * is no drop-in left at all.
186 */
187 $foreign_dropin = $wp_filesystem->exists( $drop_in ) && ! $dropin_ours;
188 if ( ! $foreign_dropin && ( $dropin_ours || $marked ) ) {
189 $config = xspeed_strip_wp_cache_define( $config );
190 $wp_filesystem->put_contents( $wp_config, $config, FS_CHMOD_FILE );
191 }
192 }
193 }
194 delete_option( 'xspeed_page_cache_ownership_receipt' );
195 flock( $ownership_lock, LOCK_UN );
196 fclose( $ownership_lock );
197 // Our own lock file, left in wp-content after everything else of ours is
198 // gone. Deleted last, after the handle is closed, so we are not
199 // unlinking a lock we are still inside. That ordering is hygiene, not a
200 // guarantee: a request already past the fopen would hold a handle to an
201 // unlinked inode. Harmless here — by this point the plugin is being
202 // removed and nothing will take the lock again.
203 $wp_filesystem->delete( WP_CONTENT_DIR . '/.xspeed-page-cache.lock' );
204 }
205