PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.0
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
← All changes | includes/core/class-deactivator.php +88 -24 1.0.22.7.0 View file →
@@ -24,22 +24,94 @@
24 24 *
25 25 * @since 1.0.0
26 26 */
27 27 class Deactivator {
28 -
28 +
29 29 /**
30 + * Which web-root artifacts deactivation removed, for activation to restore.
31 + *
32 + * Holds a subset of ['sitemap', 'robots', 'llms']. Read and deleted by
33 + * {@see Activator::restore_webroot_artifacts()}. Keep in sync with
34 + * Activator::REPUBLISH_OPTION.
35 + *
36 + * @since 2.1.0
37 + */
38 + public const REPUBLISH_OPTION = 'thinkrank_webroot_republish';
39 +
40 + /**
30 41 * Plugin deactivation tasks
31 - *
42 + *
32 43 * @return void
33 44 */
34 45 public function deactivate(): void {
35 46 $this->clear_scheduled_hooks();
36 47 $this->clear_cache();
48 + $this->remove_published_files();
37 49 $this->log_deactivation();
38 -
50 +
39 51 // Note: We don't delete user data on deactivation
40 52 // Data is only removed on uninstall
41 53 }
54 +
55 + /**
56 + * Remove physically published files so they stop serving once the plugin
57 + * is inactive.
58 + *
59 + * ThinkRank publishes its sitemap, robots.txt and llms.txt as real files in
60 + * the web root rather than serving them through rewrite rules. A leftover
61 + * file is served by the web server before WordPress boots, so it does not
62 + * merely go stale — it shadows the route of whatever the user switched to
63 + * (#510). Deactivating to trial another SEO plugin is the common way people
64 + * hit that, which is why removal belongs here and not only in uninstall.
65 + *
66 + * The stored settings and documents are deliberately untouched: only the
67 + * artifacts go. {@see Activator::restore_webroot_artifacts()} republishes
68 + * them from those settings when the plugin is switched back on.
69 + *
70 + * @since 2.1.0 Also removes the sitemap files and a generated robots.txt,
71 + * and keeps the llms.txt document instead of discarding it.
72 + *
73 + * @return void
74 + */
75 + private function remove_published_files(): void {
76 + require_once THINKRANK_PLUGIN_DIR . 'includes/cleanup-webroot.php';
77 +
78 + // Record what was actually published so reactivation restores exactly
79 + // that, and nothing else. Republishing from settings alone would write
80 + // files a site never had — every artifact defaults to enabled, so a
81 + // fresh install would start emitting a robots.txt it had not asked for.
82 + $republish = [];
83 +
84 + if (thinkrank_webroot_delete_sitemaps(thinkrank_webroot_read_sitemap_settings())['deleted']) {
85 + $republish[] = 'sitemap';
86 + }
87 +
88 + if (thinkrank_webroot_delete_robots_txt()['deleted']) {
89 + $republish[] = 'robots';
90 + }
91 +
92 + if (class_exists('ThinkRank\\SEO\\LLMs_Txt_Manager')) {
93 + // Checked up front: unpublish_static_file() reports success for a
94 + // file that was never there, which would mark a site that never
95 + // published llms.txt for republishing.
96 + $was_published = file_exists(ABSPATH . 'llms.txt');
97 +
98 + // unpublish_static_file(), not delete_llms_txt_file(): the served
99 + // file goes, the user's document stays for reactivation.
100 + if ((new \ThinkRank\SEO\LLMs_Txt_Manager())->unpublish_static_file() && $was_published) {
101 + $republish[] = 'llms';
102 + }
103 + }
104 +
105 + // Autoload off: this is read once, on the next activation.
106 + update_option(self::REPUBLISH_OPTION, $republish, false);
107 + // Static /.well-known/ OAuth discovery files (published by the MCP
108 + // self-test on hosts whose proxy intercepts that directory) — a
109 + // static copy must not keep advertising a server that is now off.
110 + if (class_exists('ThinkRank\\Mcp\\Mcp_Static_Discovery')) {
111 + \ThinkRank\Mcp\Mcp_Static_Discovery::remove();
112 + }
113 + }
42 114
43 115 /**
44 116 * Clear all scheduled hooks
45 117 *
@@ -45,25 +117,19 @@
45 117 *
46 118 * @return void
47 119 */
48 120 private function clear_scheduled_hooks(): void {
49 - $scheduled_hooks = [
50 - 'thinkrank_monthly_credit_reset',
51 - 'thinkrank_cache_cleanup',
52 - 'thinkrank_usage_analytics',
53 - ];
54 -
55 - foreach ($scheduled_hooks as $hook) {
56 - $timestamp = wp_next_scheduled($hook);
57 - if ($timestamp) {
58 - wp_unschedule_event($timestamp, $hook);
59 - }
121 + // Read from the shared manifest rather than a local copy. Deactivation
122 + // cleared 5 of the 15 hooks the plugin schedules and uninstall cleared
123 + // 2, so a removal left recurring events behind — including
124 + // thinkrank_google_token_refresh on the custom thinkrank_45min
125 + // recurrence, whose interval no longer resolves once the plugin's
126 + // cron_schedules filter is gone (#389).
127 + $manifest = require THINKRANK_PLUGIN_DIR . 'includes/cleanup-manifest.php';
128 +
129 + foreach ($manifest['cron_hooks'] as $hook) {
130 + wp_clear_scheduled_hook($hook);
60 131 }
61 -
62 - // Clear all instances of our hooks
63 - wp_clear_scheduled_hook('thinkrank_monthly_credit_reset');
64 - wp_clear_scheduled_hook('thinkrank_cache_cleanup');
65 - wp_clear_scheduled_hook('thinkrank_usage_analytics');
66 132 }
67 133
68 134 /**
69 135 * Clear plugin cache
@@ -74,19 +140,19 @@
74 140 global $wpdb;
75 141
76 142 // Clear AI cache table with proper escaping
77 143 $cache_table = $wpdb->prefix . 'thinkrank_ai_cache';
78 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin deactivation requires direct database access to check table existence
144 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Plugin deactivation requires direct database access to check table existence
79 145 $table_exists = $wpdb->get_var(
80 146 $wpdb->prepare("SHOW TABLES LIKE %s", $cache_table)
81 147 );
82 148 if ($table_exists === $cache_table) {
83 - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name is properly constructed from controlled prefix, plugin deactivation requires direct database access
149 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is properly constructed from controlled prefix, plugin deactivation requires direct database access
84 150 $wpdb->query("TRUNCATE TABLE {$cache_table}");
85 151 }
86 152
87 153 // Clear WordPress transients with proper escaping
88 - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- $wpdb->options is a WordPress core property, plugin deactivation requires direct database access
154 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $wpdb->options is a WordPress core property, plugin deactivation requires direct database access
89 155 $wpdb->query(
90 156 $wpdb->prepare(
91 157 "DELETE FROM {$wpdb->options}
92 158 WHERE option_name LIKE %s
@@ -112,10 +178,8 @@
112 178 'timestamp' => time(),
113 179 'version' => THINKRANK_VERSION,
114 180 'wp_version' => get_bloginfo('version'),
115 181 'php_version' => PHP_VERSION,
116 - 'active_plugins' => get_option('active_plugins', []),
117 - 'site_url' => get_site_url(),
118 182 ];
119 183
120 184 // Store deactivation data for potential feedback
121 185 update_option('thinkrank_last_deactivation', $deactivation_data);