| @@ -69,8 +69,26 @@ | ||
| 69 | 69 | // Settings class default and the documented UI behavior — only nuke data when |
| 70 | 70 | // the user has explicitly enabled "Delete all data on uninstall". |
| 71 | 71 | $keep_data = (bool) get_option('thinkrank_keep_data_on_uninstall', true); |
| 72 | 72 | |
| 73 | + // Files first, and regardless of $keep_data. | |
| 74 | + // | |
| 75 | + // Ordering: every ownership test in there reads state the database | |
| 76 | + // cleanup below is about to destroy — sitemap filenames come from the | |
| 77 | + // settings table, the IndexNow key file's name from an option. Run it | |
| 78 | + // after, and there is nothing left to identify our files by. | |
| 79 | + // | |
| 80 | + // Not gated on $keep_data because these are not the user's data. They | |
| 81 | + // are artifacts of a plugin that is being removed, and leaving them is | |
| 82 | + // actively harmful: a real file at /sitemap_index.xml is served by the | |
| 83 | + // web server before WordPress boots, so it goes on shadowing RankMath, | |
| 84 | + // Squirrly or core's own sitemap indefinitely — while rendering blank, | |
| 85 | + // since the XSL stylesheet it points at leaves with the plugin (#510). | |
| 86 | + // "Keep my data" means keep the settings for a reinstall, not keep a | |
| 87 | + // dead file breaking the next plugin. On reinstall they are republished | |
| 88 | + // from the settings that were kept. | |
| 89 | + self::remove_webroot_files(); | |
| 90 | + | |
| 73 | 91 | if (!$keep_data) { |
| 74 | 92 | self::remove_database_tables(); |
| 75 | 93 | self::remove_options(); |
| 76 | 94 | self::remove_user_meta(); |
| @@ -84,8 +102,38 @@ | ||
| 84 | 102 | self::mark_uninstalled(); |
| 85 | 103 | } |
| 86 | 104 | |
| 87 | 105 | /** |
| 106 | + * Delete every file ThinkRank published into the WordPress web root. | |
| 107 | + * | |
| 108 | + * The sitemap, robots.txt, llms.txt and the IndexNow key file are real files | |
| 109 | + * in `ABSPATH`, not routes, so removing the plugin has to remove them too — | |
| 110 | + * otherwise the web server keeps serving a sitemap belonging to a plugin | |
| 111 | + * that is no longer installed, and the next SEO plugin's own sitemap can | |
| 112 | + * never answer (#510). | |
| 113 | + * | |
| 114 | + * @since 2.1.0 | |
| 115 | + * | |
| 116 | + * @return void | |
| 117 | + */ | |
| 118 | + private static function remove_webroot_files(): void { | |
| 119 | + // No autoloader here — WP_UNINSTALL_PLUGIN loads this file without the | |
| 120 | + // plugin — so the removal logic comes from the same plain function file | |
| 121 | + // the deactivator and the sitemap generator use. One copy, not three. | |
| 122 | + $shared = __DIR__ . '/includes/cleanup-webroot.php'; | |
| 123 | + | |
| 124 | + if (!is_readable($shared)) { | |
| 125 | + return; | |
| 126 | + } | |
| 127 | + | |
| 128 | + require_once $shared; | |
| 129 | + | |
| 130 | + if (function_exists('thinkrank_webroot_cleanup')) { | |
| 131 | + thinkrank_webroot_cleanup(); | |
| 132 | + } | |
| 133 | + } | |
| 134 | + | |
| 135 | + /** | |
| 88 | 136 | * The shared cleanup manifest. |
| 89 | 137 | * |
| 90 | 138 | * There is no autoloader here — WP_UNINSTALL_PLUGIN loads this file without |
| 91 | 139 | * the plugin — so the cron-hook and capability lists come from a plain |
| @@ -141,9 +189,11 @@ | ||
| 141 | 189 | $wpdb->prefix . 'thinkrank_seo_local', |
| 142 | 190 | $wpdb->prefix . 'thinkrank_email_report_logs', |
| 143 | 191 | // AI Visibility Tables. These were registered in Database_Schema but |
| 144 | 192 | // never listed here, so an uninstall left them behind — bv_tasks in |
| 145 | - // particular holds the full text of every AI answer (#302). | |
| 193 | + // particular holds the full text of every AI answer (#302). The | |
| 194 | + // three Brand Visibility tables are no longer created, but sites | |
| 195 | + // that used the feature before it was removed still have them. | |
| 146 | 196 | $wpdb->prefix . 'thinkrank_ai_traffic', |
| 147 | 197 | $wpdb->prefix . 'thinkrank_brand_visibility_checks', |
| 148 | 198 | $wpdb->prefix . 'thinkrank_bv_runs', |
| 149 | 199 | $wpdb->prefix . 'thinkrank_bv_tasks', |
| @@ -209,8 +259,29 @@ | ||
| 209 | 259 | $wpdb->esc_like('_transient_timeout_thinkrank_pro_') . '%' |
| 210 | 260 | ) |
| 211 | 261 | ); |
| 212 | 262 | // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching |
| 263 | + | |
| 264 | + // Names the wildcards above cannot reach (#918). | |
| 265 | + // | |
| 266 | + // The usage tracker stores the deactivation survey under | |
| 267 | + // `wpins_deactivation_{reason,details}_{plugin}`, which `wpins_thinkrank_%` | |
| 268 | + // misses. Named exactly: widening to `wpins_%` would delete the state of | |
| 269 | + // every other plugin that ships the same telemetry SDK. | |
| 270 | + foreach (['wpins_deactivation_reason_thinkrank', 'wpins_deactivation_details_thinkrank'] as $option) { | |
| 271 | + delete_option($option); | |
| 272 | + } | |
| 273 | + | |
| 274 | + // Search Console caches that versions before 2.14.2 wrote under unprefixed | |
| 275 | + // names (2.14.2 moved them to the `thinkrank_` prefix, see #918). | |
| 276 | + // They expire within three days of the upgrade, but uninstalling inside | |
| 277 | + // that window would otherwise leave the site's search queries behind. | |
| 278 | + foreach (['7d', '30d', '90d', '14d', '60d', '180d'] as $range) { | |
| 279 | + delete_transient('analytics_dashboard_v5_' . $range); | |
| 280 | + } | |
| 281 | + foreach (['7d', '30d', '90d'] as $range) { | |
| 282 | + delete_transient('seo_opportunities_' . $range); | |
| 283 | + } | |
| 213 | 284 | } |
| 214 | 285 | |
| 215 | 286 | /** |
| 216 | 287 | * Remove user meta data |