PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.0.2
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.0.2
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / uninstall.php

uninstall.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.0.2, at uninstall.php

346 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Uninstall Script
4 *
5 * Handles complete plugin removal and cleanup
6 *
7 * @package ThinkRank
8 * @since 1.0.0
9 */
10
11 declare(strict_types=1);
12
13 // Prevent direct access
14 if (!defined('WP_UNINSTALL_PLUGIN')) {
15 exit;
16 }
17
18 /**
19 * ThinkRank Uninstaller Class
20 *
21 * Single Responsibility: Handle complete plugin removal
22 */
23 class ThinkRank_Uninstaller {
24
25 /**
26 * Option that records a deliberate uninstall.
27 *
28 * Keep in sync with ThinkRank\Core\Activator::UNINSTALLED_OPTION and with
29 * ThinkRank Pro's Free_Plugin_Installer.
30 */
31 private const UNINSTALLED_OPTION = 'thinkrank_uninstalled';
32
33 /**
34 * Run uninstall process
35 *
36 * @return void
37 */
38 public static function uninstall(): void {
39 // Everything below resolves through $wpdb->prefix and $wpdb->options,
40 // i.e. the current blog. On a network install WordPress runs this once,
41 // so every other site kept its tables, its options and its stored
42 // credentials — API keys and Google refresh tokens included (#399).
43 if (is_multisite()) {
44 $site_ids = get_sites([
45 'fields' => 'ids',
46 'number' => 0,
47 'update_site_meta_cache' => false,
48 ]);
49
50 foreach ($site_ids as $site_id) {
51 switch_to_blog((int) $site_id);
52 self::uninstall_site();
53 restore_current_blog();
54 }
55
56 return;
57 }
58
59 self::uninstall_site();
60 }
61
62 /**
63 * Remove everything ThinkRank created on the current blog.
64 *
65 * @return void
66 */
67 private static function uninstall_site(): void {
68 // Check if user wants to keep data. Default to TRUE (preserve) to match the
69 // Settings class default and the documented UI behavior — only nuke data when
70 // the user has explicitly enabled "Delete all data on uninstall".
71 $keep_data = (bool) get_option('thinkrank_keep_data_on_uninstall', true);
72
73 if (!$keep_data) {
74 self::remove_database_tables();
75 self::remove_options();
76 self::remove_user_meta();
77 self::remove_post_meta();
78 self::remove_term_meta();
79 }
80
81 self::clear_caches();
82 self::remove_cron_jobs();
83 self::remove_capabilities();
84 self::mark_uninstalled();
85 }
86
87 /**
88 * The shared cleanup manifest.
89 *
90 * There is no autoloader here — WP_UNINSTALL_PLUGIN loads this file without
91 * the plugin — so the cron-hook and capability lists come from a plain
92 * array file that the deactivator reads too.
93 *
94 * @return array{cron_hooks: string[], capabilities: string[]}
95 */
96 private static function manifest(): array {
97 return require __DIR__ . '/includes/cleanup-manifest.php';
98 }
99
100 /**
101 * Record that the user deliberately uninstalled the plugin.
102 *
103 * ThinkRank Pro auto-installs and activates the free plugin whenever it
104 * finds it missing, which silently re-ran the activator and recreated every
105 * table this uninstall had just dropped. Pro reads this marker and stops
106 * auto-installing, falling back to its "Install ThinkRank" notice, so a
107 * deliberate removal stays removed until the user asks for it back.
108 *
109 * Written last, after remove_options() has wiped the `thinkrank_` namespace,
110 * and cleared again by Activator on the next activation.
111 *
112 * @return void
113 */
114 private static function mark_uninstalled(): void {
115 delete_option(self::UNINSTALLED_OPTION);
116 add_option(self::UNINSTALLED_OPTION, time(), '', 'no');
117 }
118
119 /**
120 * Remove custom database tables
121 *
122 * @return void
123 */
124 private static function remove_database_tables(): void {
125 global $wpdb;
126
127 $tables = [
128 // AI/Core Tables (from Database class)
129 $wpdb->prefix . 'thinkrank_ai_cache',
130 $wpdb->prefix . 'thinkrank_ai_usage',
131 $wpdb->prefix . 'thinkrank_content_briefs',
132 $wpdb->prefix . 'thinkrank_seo_scores',
133 $wpdb->prefix . 'thinkrank_seo_performance',
134 $wpdb->prefix . 'thinkrank_instant_indexing_logs',
135 // SEO Tables (from Database_Schema)
136 $wpdb->prefix . 'thinkrank_seo_settings',
137 $wpdb->prefix . 'thinkrank_seo_analysis',
138 $wpdb->prefix . 'thinkrank_seo_keywords',
139 $wpdb->prefix . 'thinkrank_seo_schema',
140 $wpdb->prefix . 'thinkrank_seo_social',
141 $wpdb->prefix . 'thinkrank_seo_local',
142 $wpdb->prefix . 'thinkrank_email_report_logs',
143 // AI Visibility Tables. These were registered in Database_Schema but
144 // never listed here, so an uninstall left them behind — bv_tasks in
145 // particular holds the full text of every AI answer (#302).
146 $wpdb->prefix . 'thinkrank_ai_traffic',
147 $wpdb->prefix . 'thinkrank_brand_visibility_checks',
148 $wpdb->prefix . 'thinkrank_bv_runs',
149 $wpdb->prefix . 'thinkrank_bv_tasks',
150 // Rank Tracker, Redirections, Broken Links and Local SEO tables are
151 // created and dropped by ThinkRank Pro's own uninstaller. Free used to
152 // drop them here, which destroyed a still-active Pro install's data
153 // when only the free plugin was removed (#384).
154 ];
155
156 foreach ($tables as $table) {
157 // Check WordPress version for %i support (introduced in 6.2)
158 if (version_compare($GLOBALS['wp_version'], '6.2', '>=')) {
159 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnsupportedIdentifierPlaceholder,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange
160 $wpdb->query($wpdb->prepare("DROP TABLE IF EXISTS %i", $table));
161 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnsupportedIdentifierPlaceholder,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange
162 } else {
163 // Fallback for older WordPress versions - table name is from our controlled list
164 $escaped_table = esc_sql($table);
165 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange
166 $wpdb->query("DROP TABLE IF EXISTS `{$escaped_table}`");
167 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange
168 }
169 }
170 }
171
172 /**
173 * Remove plugin options
174 *
175 * @return void
176 */
177 private static function remove_options(): void {
178 // Remove all ThinkRank options using wildcard delete for completeness.
179 //
180 // `thinkrank_%` also matches `thinkrank_pro_%`, so this used to delete a
181 // still-active Pro install's license key and every module setting (#384).
182 // Pro ships its own uninstaller and owns that namespace exclusively, so
183 // every pattern here excludes it.
184 global $wpdb;
185 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Uninstall cleanup requires direct database access to remove all plugin options
186 $wpdb->query(
187 $wpdb->prepare(
188 "DELETE FROM {$wpdb->options}
189 WHERE (option_name LIKE %s AND option_name NOT LIKE %s)
190 OR option_name LIKE %s",
191 $wpdb->esc_like('thinkrank_') . '%',
192 $wpdb->esc_like('thinkrank_pro_') . '%',
193 // Appsero / WP Insights telemetry options (wpins_thinkrank_*)
194 $wpdb->esc_like('wpins_thinkrank_') . '%'
195 )
196 );
197 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
198
199 // Remove transients (prefixed with _transient_, not caught by options wildcard)
200 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Core table name is safe, uninstall cleanup requires direct database access
201 $wpdb->query(
202 $wpdb->prepare(
203 "DELETE FROM {$wpdb->options}
204 WHERE (option_name LIKE %s AND option_name NOT LIKE %s)
205 OR (option_name LIKE %s AND option_name NOT LIKE %s)",
206 $wpdb->esc_like('_transient_thinkrank_') . '%',
207 $wpdb->esc_like('_transient_thinkrank_pro_') . '%',
208 $wpdb->esc_like('_transient_timeout_thinkrank_') . '%',
209 $wpdb->esc_like('_transient_timeout_thinkrank_pro_') . '%'
210 )
211 );
212 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
213 }
214
215 /**
216 * Remove user meta data
217 *
218 * @return void
219 */
220 private static function remove_user_meta(): void {
221 global $wpdb;
222
223 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Core table name is safe, uninstall cleanup requires direct database access
224 $wpdb->query(
225 $wpdb->prepare(
226 "DELETE FROM {$wpdb->usermeta}
227 WHERE meta_key LIKE %s
228 OR meta_key LIKE %s",
229 $wpdb->esc_like('thinkrank_') . '%',
230 $wpdb->esc_like('_thinkrank_') . '%'
231 )
232 );
233 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
234 }
235
236 /**
237 * Remove post meta data
238 *
239 * @return void
240 */
241 private static function remove_post_meta(): void {
242 global $wpdb;
243
244 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Core table name is safe, uninstall cleanup requires direct database access
245 $wpdb->query(
246 $wpdb->prepare(
247 "DELETE FROM {$wpdb->postmeta}
248 WHERE meta_key LIKE %s
249 OR meta_key LIKE %s",
250 $wpdb->esc_like('thinkrank_') . '%',
251 $wpdb->esc_like('_thinkrank_') . '%'
252 )
253 );
254 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
255 }
256
257
258 /**
259 * Remove term meta data
260 *
261 * The plugin writes term meta from the term UI, the abilities API
262 * (Update_Term_Seo) and the importer (Snapshot_Migrator) — SEO title,
263 * meta description, the robots payload and the `_thinkrank_imported_from`
264 * marker — and there was no counterpart to remove_post_meta(), so all of
265 * it outlived the plugin (#399).
266 *
267 * @return void
268 */
269 private static function remove_term_meta(): void {
270 global $wpdb;
271
272 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Core table name is safe, uninstall cleanup requires direct database access
273 $wpdb->query(
274 $wpdb->prepare(
275 "DELETE FROM {$wpdb->termmeta}
276 WHERE meta_key LIKE %s
277 OR meta_key LIKE %s",
278 $wpdb->esc_like('thinkrank_') . '%',
279 $wpdb->esc_like('_thinkrank_') . '%'
280 )
281 );
282 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
283 }
284
285 /**
286 * Clear all caches
287 *
288 * @return void
289 */
290 private static function clear_caches(): void {
291 // Clear ThinkRank-specific object cache group
292 if (function_exists('wp_cache_flush_group')) {
293 wp_cache_flush_group('thinkrank');
294 }
295 }
296
297 /**
298 * Remove scheduled cron jobs
299 *
300 * @return void
301 */
302 private static function remove_cron_jobs(): void {
303 // Was 2 hooks against the 15 the plugin schedules, and weaker than the
304 // deactivator's 5 — so uninstalling without deactivating first left
305 // even more behind (#389).
306 foreach (self::manifest()['cron_hooks'] as $job) {
307 wp_clear_scheduled_hook($job);
308 }
309 }
310
311 /**
312 * Remove custom capabilities
313 *
314 * @return void
315 */
316 private static function remove_capabilities(): void {
317 // Runs even when the user keeps their data, so the capability-sync marker
318 // has to go with it. Leaving it behind would make Capability_Manager::ensure()
319 // short-circuit on the next install and never re-grant thinkrank_access,
320 // locking administrators out of the admin menu.
321 delete_option('thinkrank_caps_version');
322
323 // From the shared manifest. The hand-mirrored copy that used to live
324 // here had drifted from Capability_Manager::capabilities() by four
325 // slugs — thinkrank_ai_insights, thinkrank_redirections,
326 // thinkrank_broken_links and thinkrank_woocommerce were still granted
327 // to every role after an uninstall (#399). The manifest also carries
328 // the pre-Role-Manager slugs so historical roles are fully cleaned.
329 $capabilities = self::manifest()['capabilities'];
330
331 $roles = wp_roles();
332
333 foreach ($roles->roles as $role_name => $role_info) {
334 $role = get_role($role_name);
335 if ($role) {
336 foreach ($capabilities as $cap) {
337 $role->remove_cap($cap);
338 }
339 }
340 }
341 }
342 }
343
344 // Run the uninstaller
345 ThinkRank_Uninstaller::uninstall();
346