PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / trunk
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO vtrunk
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 1.11.0 All 47 releases
thinkrank / includes / core / class-deactivator.php

class-deactivator.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO trunk, at includes/core/class-deactivator.php

191 lines 7.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Deactivator Class
4 *
5 * Handles plugin deactivation tasks
6 *
7 * @package ThinkRank\Core
8 * @since 1.0.0
9 */
10
11 declare(strict_types=1);
12
13 namespace ThinkRank\Core;
14
15 // Prevent direct access
16 if (!defined('ABSPATH')) {
17 exit;
18 }
19
20 /**
21 * Deactivator Class
22 *
23 * Single Responsibility: Handle plugin deactivation tasks only
24 *
25 * @since 1.0.0
26 */
27 class Deactivator {
28
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 /**
41 * Plugin deactivation tasks
42 *
43 * @return void
44 */
45 public function deactivate(): void {
46 $this->clear_scheduled_hooks();
47 $this->clear_cache();
48 $this->remove_published_files();
49 $this->log_deactivation();
50
51 // Note: We don't delete user data on deactivation
52 // Data is only removed on uninstall
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 }
114
115 /**
116 * Clear all scheduled hooks
117 *
118 * @return void
119 */
120 private function clear_scheduled_hooks(): void {
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);
131 }
132 }
133
134 /**
135 * Clear plugin cache
136 *
137 * @return void
138 */
139 private function clear_cache(): void {
140 global $wpdb;
141
142 // Clear AI cache table with proper escaping
143 $cache_table = $wpdb->prefix . 'thinkrank_ai_cache';
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
145 $table_exists = $wpdb->get_var(
146 $wpdb->prepare("SHOW TABLES LIKE %s", $cache_table)
147 );
148 if ($table_exists === $cache_table) {
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
150 $wpdb->query("TRUNCATE TABLE {$cache_table}");
151 }
152
153 // Clear WordPress transients with proper escaping
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
155 $wpdb->query(
156 $wpdb->prepare(
157 "DELETE FROM {$wpdb->options}
158 WHERE option_name LIKE %s
159 OR option_name LIKE %s",
160 $wpdb->esc_like('_transient_thinkrank_') . '%',
161 $wpdb->esc_like('_transient_timeout_thinkrank_') . '%'
162 )
163 );
164
165 // Clear object cache if available
166 if (function_exists('wp_cache_flush_group')) {
167 wp_cache_flush_group('thinkrank');
168 }
169 }
170
171 /**
172 * Log deactivation for analytics
173 *
174 * @return void
175 */
176 private function log_deactivation(): void {
177 $deactivation_data = [
178 'timestamp' => time(),
179 'version' => THINKRANK_VERSION,
180 'wp_version' => get_bloginfo('version'),
181 'php_version' => PHP_VERSION,
182 ];
183
184 // Store deactivation data for potential feedback
185 update_option('thinkrank_last_deactivation', $deactivation_data);
186
187 // Remove activation flag
188 delete_option('thinkrank_activated');
189 }
190 }
191