PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.13.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.13.0
2.13.0 2.12.0 2.11.0 2.10.0 2.9.0 2.8.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 All 54 releases
← All changes | includes/admin/importers/class-import-detector.php +148 -1 2.1.0 → 2.13.0 View file →
@@ -30,8 +30,13 @@
30 30 */
31 31 private const CACHE_KEY = 'thinkrank_import_detection';
32 32
33 33 /**
34 + * Transient key for caching the native (ThinkRank's own data) counts
35 + */
36 + private const NATIVE_CACHE_KEY = 'thinkrank_export_detection';
37 +
38 + /**
34 39 * Cache TTL in seconds (1 hour)
35 40 */
36 41 private const CACHE_TTL = 3600;
37 42
@@ -63,8 +68,24 @@
63 68 'meta_prefix' => '',
64 69 'option_keys' => ['aioseo_options'],
65 70 'plugin_files' => ['all-in-one-seo-pack/all_in_one_seo_pack.php', 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php'],
66 71 ],
72 + 'squirrly' => [
73 + 'name' => 'Squirrly SEO',
74 + // Squirrly keeps SEO in its own `qss` table, not postmeta (#740);
75 + // `_sq_` only covers its few fallback meta keys.
76 + 'meta_prefix' => '_sq_',
77 + 'option_keys' => ['sq_options'],
78 + 'plugin_files' => ['squirrly-seo/squirrly.php'],
79 + ],
80 + 'slimseo' => [
81 + 'name' => 'Slim SEO',
82 + // One `slim_seo` meta array per post/term, plus primary-term keys
83 + // and option-stored redirects (#886) — counted by the exporter.
84 + 'meta_prefix' => 'slim_seo',
85 + 'option_keys' => ['slim_seo', 'ss_redirects', 'slim_seo_pro', 'slim_seo_schemas'],
86 + 'plugin_files' => ['slim-seo/slim-seo.php', 'slim-seo-pro/slim-seo-pro.php'],
87 + ],
67 88 ];
68 89
69 90 /**
70 91 * Detect all source plugins present in the database
@@ -94,8 +115,44 @@
94 115 return $detected;
95 116 }
96 117
97 118 /**
119 + * Detect ThinkRank's own exportable data.
120 + *
121 + * Deliberately NOT part of the PLUGINS registry that detect() walks. That
122 + * registry describes plugins to migrate FROM: every entry is gated on
123 + * is_source_active() (which would reject us), feeds the "import from" cards
124 + * in the UI, and — most importantly — drives cleanup()'s prefix maps, which
125 + * delete the listed plugin's live data. Keeping the native source on its
126 + * own path avoids all three problems instead of special-casing each.
127 + *
128 + * @param bool $use_cache Whether to use cached results
129 + * @return array|null Detection result, or null when there is nothing to export
130 + */
131 + public function detect_native(bool $use_cache = true): ?array {
132 + if ($use_cache) {
133 + $cached = get_transient(self::NATIVE_CACHE_KEY);
134 + if ($cached !== false) {
135 + return is_array($cached) ? $cached : null;
136 + }
137 + }
138 +
139 + $exporter = new Thinkrank_Exporter();
140 + $counts = $exporter->get_available_types();
141 +
142 + $result = [
143 + 'plugin' => $exporter->get_plugin_slug(),
144 + 'plugin_name' => $exporter->get_plugin_name(),
145 + 'counts' => $counts,
146 + 'total' => array_sum($counts),
147 + ];
148 +
149 + set_transient(self::NATIVE_CACHE_KEY, $result, self::CACHE_TTL);
150 +
151 + return $result;
152 + }
153 +
154 + /**
98 155 * Clear the detection cache
99 156 *
100 157 * @return void
101 158 */
@@ -100,8 +157,9 @@
100 157 * @return void
101 158 */
102 159 public function clear_cache(): void {
103 160 delete_transient(self::CACHE_KEY);
161 + delete_transient(self::NATIVE_CACHE_KEY);
104 162 }
105 163
106 164 /**
107 165 * Detect a single plugin's data presence
@@ -116,10 +174,18 @@
116 174 // Only surface source plugins that are currently installed AND active.
117 175 // Migrating from a plugin the user no longer runs isn't actionable, so
118 176 // leftover data from a deactivated/uninstalled plugin is intentionally
119 177 // excluded from the migration screen.
178 + //
179 + // The one exception is Rank Math's FAQ / HowTo blocks. Those live in
180 + // post_content, and deactivating Rank Math is precisely what breaks
181 + // them: the blocks turn into "unsupported block" in the editor and lose
182 + // their schema on the front end. Hiding the only screen that can repair
183 + // them, at the exact moment they need repairing, left users with no way
184 + // out at all (#777) — so an inactive Rank Math stays listed whenever
185 + // convertible blocks remain, carrying that type and nothing else.
120 186 if (!$this->is_source_active($config['plugin_files'] ?? [])) {
121 - return null;
187 + return $this->detect_orphaned_blocks($slug, $config);
122 188 }
123 189
124 190 $counts = [];
125 191 $total = 0;
@@ -126,8 +192,15 @@
126 192
127 193 if ($slug === 'aioseo') {
128 194 // AIOSEO uses a custom table
129 195 $counts = $this->detect_aioseo();
196 + } elseif ($slug === 'squirrly') {
197 + // Squirrly uses a custom table too; the exporter classifies rows.
198 + $counts = $this->detect_squirrly();
199 + } elseif ($slug === 'slimseo') {
200 + // Slim SEO's redirects and 404 log live outside postmeta, and its
201 + // primary terms under a second key; the exporter counts all of it.
202 + $counts = $this->detect_slimseo();
130 203 } else {
131 204 // Standard postmeta-based plugins
132 205 $prefix = $config['meta_prefix'];
133 206
@@ -161,8 +234,18 @@
161 234 // when the exporter supports it (Rank Math Pro's redirects + 404
162 235 // Monitor and Yoast Premium's redirects were never offered).
163 236 $counts = array_merge($counts, $this->detect_redirect_data($slug));
164 237
238 + // FAQ / HowTo blocks, offered while Rank Math is still active too, so
239 + // the wizard converts them on the way out rather than leaving the user
240 + // to discover the breakage after deactivation.
241 + if ('rankmath' === $slug) {
242 + $block_count = Block_Converter::count_posts();
243 + if ($block_count > 0) {
244 + $counts[Block_Converter::TYPE] = $block_count;
245 + }
246 + }
247 +
165 248 // Check for settings
166 249 $has_settings = false;
167 250 foreach ($config['option_keys'] as $option_key) {
168 251 if (get_option($option_key, null) !== null) {
@@ -192,8 +275,39 @@
192 275 ];
193 276 }
194 277
195 278 /**
279 + * Detection result for a deactivated source that has left convertible
280 + * content blocks behind, or null when there is nothing to repair.
281 + *
282 + * Only the block type is reported. The plugin's postmeta and settings are
283 + * deliberately still withheld: migrating those from a plugin the site no
284 + * longer runs is the case the active-only rule exists to prevent, and it is
285 + * not what broke.
286 + *
287 + * @param string $slug Plugin slug.
288 + * @param array $config Plugin configuration.
289 + * @return array|null
290 + */
291 + private function detect_orphaned_blocks(string $slug, array $config): ?array {
292 + if ('rankmath' !== $slug) {
293 + return null;
294 + }
295 +
296 + $count = Block_Converter::count_posts();
297 + if ($count < 1) {
298 + return null;
299 + }
300 +
301 + return [
302 + 'plugin' => $slug,
303 + 'plugin_name' => $config['name'],
304 + 'counts' => [Block_Converter::TYPE => $count],
305 + 'total' => $count,
306 + ];
307 + }
308 +
309 + /**
196 310 * Whether any of a source plugin's known main files is active.
197 311 *
198 312 * Checks both single-site and network activation. A plugin that is merely
199 313 * installed but not active returns false.
@@ -261,8 +375,41 @@
261 375 if ($redirects > 0) {
262 376 $counts['redirections'] = $redirects;
263 377 }
264 378 }
379 + // Squirrly's Advanced Pack redirects are counted by the exporter in
380 + // detect_squirrly(), since only its `url` actions are redirects.
381 +
382 + return $counts;
383 + }
384 +
385 + /**
386 + * Detect Squirrly SEO data (custom `qss` table + Advanced Pack redirects).
387 + *
388 + * Delegated to the exporter: the table mixes posts, terms, author
389 + * profiles and the homepage, and only the exporter knows how to tell
390 + * them apart from the serialized `post` column.
391 + *
392 + * @return array Counts array
393 + */
394 + private function detect_squirrly(): array {
395 + $exporter = new Squirrly_Exporter();
396 + $counts = $exporter->get_available_types();
397 + // Settings are added by the caller from option_keys.
398 + unset($counts['settings']);
399 +
400 + return $counts;
401 + }
402 +
403 + /**
404 + * Detect Slim SEO data: per-object meta, redirects and the 404 log.
405 + *
406 + * @return array Counts array
407 + */
408 + private function detect_slimseo(): array {
409 + $counts = (new Slim_SEO_Exporter())->get_available_types();
410 + // Settings are added by the caller from option_keys.
411 + unset($counts['settings']);
265 412
266 413 return $counts;
267 414 }
268 415