PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / trunk
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN vtrunk
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 1.2.1 1.2.2 1.2.3
xspeed / uninstall.php

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

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