PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.0
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.0
1.4.1 1.4.0 1.3.7 1.3.6 1.3.5 1.3.4 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 All 35 releases
← All changes | uninstall.php +214 -7 1.0.2 → 1.4.0 View file →
@@ -14,11 +14,143 @@
14 14 /**
15 15 * Run all uninstall cleanup. Wrapped in a function so locals don't pollute global scope.
16 16 */
17 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 + */
18 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_rules_inputs' );
57 + // Per-user, so it needs the meta sweep rather than delete_option: every
58 + // admin who ever confirmed they pasted the server rules has one.
59 + if ( function_exists( 'delete_metadata' ) ) {
60 + delete_metadata( 'user', 0, 'xspeed_nginx_rules_copied', '', true );
61 + }
62 + delete_option( 'xspeed_redirect_to_onboarding' );
63 + delete_option( 'xspeed_preloader_firewall_block' );
64 + delete_option( 'xspeed_onboarding_complete' );
65 + // Provenance a host plugin wrote before it activated us, and the profile
66 + // that install came up with.
67 + delete_option( 'xspeed_installed_by' );
68 + delete_option( 'xspeed_installer' );
69 + delete_option( 'xspeed_install_profile' );
70 + // Written by the copy-vendored Setup that host plugins used to carry
71 + // before xSpeed seeded its own conflict-safe profile on activation. We no
72 + // longer write it; an older host may have, and it is ours to clean up.
73 + delete_option( 'xspeed_setup_snapshot' );
19 74 delete_option( 'xspeed_stats' );
75 + delete_option( 'xspeed_gc_cursor' );
76 + delete_option( 'xspeed_mcp_rl_gen' );
77 + wp_clear_scheduled_hook( 'xspeed_gc' );
20 78
79 + global $wpdb;
80 +
81 + // Hit counters and the Hub attachment flag — ours, and simply never named
82 + // here before. xspeed_hit_buffer is BOTH an option and a transient of the
83 + // same name (Hit_Counter uses one string for the memory buffer and the
84 + // durable counter), so both stores need clearing.
85 + delete_option( 'xspeed_hit_buffer' );
86 + delete_transient( 'xspeed_hit_buffer' );
87 + delete_option( 'xspeed_hit_daily' );
88 + delete_option( 'xspeed_hub_site_attached' );
89 +
90 + // Usage-tracking state, which lives in the shared WP Insights rows rather
91 + // than under our own prefix. Left behind, the consent key survives an
92 + // uninstall: a REINSTALL then reads as already opted in before the wizard
93 + // has asked anything, and a later deactivation posts a diagnostic payload
94 + // for a consent this install never collected (#439).
95 + //
96 + // The two keyed rows are SHARED with sibling WPDeveloper plugins, so only
97 + // our own key comes out and the row itself is deleted only once nothing
98 + // else is using it. Deleting them outright would wipe another plugin's
99 + // consent state.
100 + foreach ( array( 'wpins_allow_tracking', 'wpins_last_track_time' ) as $shared ) {
101 + $value = get_option( $shared );
102 + if ( ! is_array( $value ) ) {
103 + continue; // Absent, or a shape we did not write — leave it alone.
104 + }
105 + unset( $value['xspeed'] );
106 + if ( empty( $value ) ) {
107 + delete_option( $shared );
108 + } else {
109 + update_option( $shared, $value );
110 + }
111 + }
112 + // The deactivation-feedback payload. Normally consumed-then-deleted by the
113 + // tracker's own deactivation send, but that only happens when consent
114 + // passes and the send succeeds — a user who deactivates without consent
115 + // leaves both rows behind, and they are exactly the orphaned diagnostic
116 + // payload #439 describes.
117 + delete_option( 'wpins_deactivation_reason_xspeed' );
118 + delete_option( 'wpins_deactivation_details_xspeed' );
119 + // The tracker's recurring send. Cleared on opt-out, but nothing guarantees
120 + // an opt-out ever happened before the uninstall.
121 + wp_clear_scheduled_hook( 'xspeed_do_weekly_action' );
122 + // Health's edge-mode probe: its pending run and its stored verdict.
123 + wp_clear_scheduled_hook( 'xspeed_edge_mode_probe_refresh' );
124 + delete_transient( 'xspeed_edge_mode_probe' );
125 + delete_transient( 'xspeed_edge_mode_probe_token' );
126 + // Our own WP Insights rows: the site id, the original URL, and the last
127 + // payload — whose name embeds the site id. The payload names are
128 + // derivable from the id, but a failed earlier uninstall or a renamed row
129 + // shape would strand them, so sweep the prefix. `xspeed-pro`'s rows
130 + // survive this only because its slug's HYPHEN doesn't match the
131 + // underscore in `wpins_xspeed_%` — that separator is load-bearing.
132 + delete_option( 'wpins_xspeed_site_id' );
133 + delete_option( 'wpins_xspeed_original_url' );
134 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- options API has no prefix delete; uninstall only.
135 + $wpins_rows = $wpdb->get_col(
136 + $wpdb->prepare(
137 + "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
138 + $wpdb->esc_like( 'wpins_xspeed_' ) . '%'
139 + )
140 + );
141 + foreach ( (array) $wpins_rows as $wpins_row ) {
142 + delete_option( $wpins_row );
143 + }
144 +
145 + // Score history — the plugin's own table, plus the legacy option the
146 + // table was migrated from (kept on upgrade so a bad migration is
147 + // recoverable; there is nothing to recover on uninstall).
148 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- own table, uninstall.
149 + $wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'xspeed_scores' );
150 + delete_option( 'xspeed_score_schema' );
151 + delete_option( 'xspeed_score_history' );
152 +
21 153 if ( ! function_exists( 'WP_Filesystem' ) ) {
22 154 require_once ABSPATH . 'wp-admin/includes/file.php';
23 155 }
24 156 WP_Filesystem();
@@ -31,8 +163,30 @@
31 163 if ( $wp_filesystem->is_dir( $cache_dir ) ) {
32 164 $wp_filesystem->delete( $cache_dir, true );
33 165 }
34 166
167 + // nginx hit log (FBS-82478). It lives under uploads/xspeed/, NOT the
168 + // cache dir, precisely so that a pasted nginx `access_log` directive
169 + // pointing at it does NOT get its parent directory deleted here — if it
170 + // did, `nginx -t` would fail [emerg] and refuse to (re)start, taking
171 + // down EVERY vhost on the host until someone manually finds the orphaned
172 + // directive. We therefore EMPTY the log file but DELIBERATELY LEAVE THE
173 + // DIRECTORY in place, so any still-pasted directive keeps a valid,
174 + // openable target after the plugin is gone. (A stray empty dir is
175 + // harmless; a broken nginx is not.) Users are also warned in the admin
176 + // UI to remove the snippet before uninstalling.
177 + $hits_log = WP_CONTENT_DIR . '/uploads/xspeed/hits.log';
178 + if ( function_exists( 'wp_upload_dir' ) ) {
179 + $uploads = wp_upload_dir( null, false );
180 + if ( is_array( $uploads ) && empty( $uploads['error'] ) && ! empty( $uploads['basedir'] ) ) {
181 + $hits_log = rtrim( (string) $uploads['basedir'], '/' ) . '/xspeed/hits.log';
182 + }
183 + }
184 + if ( $wp_filesystem->exists( $hits_log ) ) {
185 + // Truncate, don't delete the dir — keep the access_log target openable.
186 + $wp_filesystem->put_contents( $hits_log, '', FS_CHMOD_FILE );
187 + }
188 +
35 189 // Static-cache tree — separate from the flat-hash cache dir, holds
36 190 // the {host}/{path}/index.html files the .htaccess rewrite block
37 191 // serves directly. Same teardown rules as XSPEED_CACHE_DIR.
38 192 $static_dir = WP_CONTENT_DIR . '/cache/xspeed-static';
@@ -50,21 +204,74 @@
50 204 }
51 205 insert_with_markers( $htaccess, 'xSpeed Static Cache', array() );
52 206 }
53 207
54 - $drop_in = WP_CONTENT_DIR . '/advanced-cache.php';
208 + // Serialize the shared drop-in/config teardown with Cache::toggle(). If
209 + // the lock is unavailable, leave shared state and its receipt untouched.
210 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen,WordPress.PHP.NoSilencedErrors.Discouraged -- flock requires a local handle; failure is fail-closed.
211 + $ownership_lock = @fopen( WP_CONTENT_DIR . '/.xspeed-page-cache.lock', 'c+' );
212 + if ( ! is_resource( $ownership_lock ) || ! flock( $ownership_lock, LOCK_EX ) ) {
213 + return;
214 + }
215 +
216 + require_once __DIR__ . '/includes/wp-cache-constant.php';
217 + $drop_in = WP_CONTENT_DIR . '/advanced-cache.php';
218 + $dropin_ours = false;
55 219 if ( $wp_filesystem->exists( $drop_in ) ) {
56 - $contents = $wp_filesystem->get_contents( $drop_in );
57 - if ( is_string( $contents ) && false !== strpos( $contents, 'XSPEED_DROPIN' ) ) {
220 + $contents = $wp_filesystem->get_contents( $drop_in );
221 + $dropin_ours = is_string( $contents ) && xspeed_has_canonical_dropin_signature( $contents );
222 + if ( $dropin_ours ) {
58 223 $wp_filesystem->delete( $drop_in );
59 224 }
60 225 }
61 226
62 - $wp_config = ABSPATH . 'wp-config.php';
63 - if ( defined( 'WP_CACHE' ) && WP_CACHE && $wp_filesystem->is_writable( $wp_config ) ) {
227 + $wp_config = file_exists( ABSPATH . 'wp-config.php' ) ? ABSPATH . 'wp-config.php' : dirname( ABSPATH ) . '/wp-config.php';
228 + // `defined()` alone, not `&& WP_CACHE`: the old truthiness test skipped
229 + // the removal whenever the constant was false/0 — exactly the orphan we
230 + // are here to clean up — while still entering it for TRUE/1, where the
231 + // hardcoded-lowercase regex then matched nothing and reported success.
232 + // Strip whatever spelling is there. (#9)
233 + //
234 + // Gated on the drop-in being ours: if another caching plugin holds
235 + // advanced-cache.php, WP_CACHE is the switch that loads THEIR file, and
236 + // stripping it on our way out would silently disable their page cache.
237 + if ( $wp_filesystem->is_writable( $wp_config ) ) {
64 238 $config = $wp_filesystem->get_contents( $wp_config );
65 239 if ( is_string( $config ) ) {
66 - $config = preg_replace( "/define\\(\\s*['\"]WP_CACHE['\"]\\s*,\\s*true\\s*\\);\\s*\\n?/", '', $config );
67 - $wp_filesystem->put_contents( $wp_config, $config, FS_CHMOD_FILE );
240 + // Same helper Cache::set_wp_cache_constant() uses — one pattern,
241 + // one place. uninstall.php loads no plugin classes, hence a
242 + // plain requirable function rather than a method.
243 + require_once __DIR__ . '/includes/wp-cache-constant.php';
244 + $receipt = get_option( 'xspeed_page_cache_ownership_receipt', '' );
245 + $marked = xspeed_wp_cache_receipt_matches( $config, $receipt );
246 + /*
247 + * A receipt proves WE wrote the line; it does not prove the line
248 + * is still ours to remove. If a competitor has since taken over
249 + * advanced-cache.php, WP_CACHE is what loads THEIR drop-in — they
250 + * had no reason to touch an already-true define, so our receipt
251 + * comment is still sitting beside it. Stripping on the receipt
252 + * alone silently stopped their live page cache.
253 + *
254 + * A foreign drop-in therefore vetoes the removal outright, which
255 + * is the same rule Cache::set_wp_cache_constant() applies in the
256 + * running plugin; only this path was missing it. `$dropin_ours`
257 + * covers the file we just deleted, `$marked` the case where there
258 + * is no drop-in left at all.
259 + */
260 + $foreign_dropin = $wp_filesystem->exists( $drop_in ) && ! $dropin_ours;
261 + if ( ! $foreign_dropin && ( $dropin_ours || $marked ) ) {
262 + $config = xspeed_strip_wp_cache_define( $config );
263 + $wp_filesystem->put_contents( $wp_config, $config, FS_CHMOD_FILE );
264 + }
68 265 }
69 266 }
267 + delete_option( 'xspeed_page_cache_ownership_receipt' );
268 + flock( $ownership_lock, LOCK_UN );
269 + fclose( $ownership_lock );
270 + // Our own lock file, left in wp-content after everything else of ours is
271 + // gone. Deleted last, after the handle is closed, so we are not
272 + // unlinking a lock we are still inside. That ordering is hygiene, not a
273 + // guarantee: a request already past the fopen would hold a handle to an
274 + // unlinked inode. Harmless here — by this point the plugin is being
275 + // removed and nothing will take the lock again.
276 + $wp_filesystem->delete( WP_CONTENT_DIR . '/.xspeed-page-cache.lock' );
70 277 }