PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.11.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.11.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 trunk 1.0.0 All 52 releases
← All changes | includes/admin/importers/class-import-detector.php +125 -1 1.28.0 → 2.11.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,16 @@
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 + ],
67 80 ];
68 81
69 82 /**
70 83 * Detect all source plugins present in the database
@@ -94,8 +107,44 @@
94 107 return $detected;
95 108 }
96 109
97 110 /**
111 + * Detect ThinkRank's own exportable data.
112 + *
113 + * Deliberately NOT part of the PLUGINS registry that detect() walks. That
114 + * registry describes plugins to migrate FROM: every entry is gated on
115 + * is_source_active() (which would reject us), feeds the "import from" cards
116 + * in the UI, and — most importantly — drives cleanup()'s prefix maps, which
117 + * delete the listed plugin's live data. Keeping the native source on its
118 + * own path avoids all three problems instead of special-casing each.
119 + *
120 + * @param bool $use_cache Whether to use cached results
121 + * @return array|null Detection result, or null when there is nothing to export
122 + */
123 + public function detect_native(bool $use_cache = true): ?array {
124 + if ($use_cache) {
125 + $cached = get_transient(self::NATIVE_CACHE_KEY);
126 + if ($cached !== false) {
127 + return is_array($cached) ? $cached : null;
128 + }
129 + }
130 +
131 + $exporter = new Thinkrank_Exporter();
132 + $counts = $exporter->get_available_types();
133 +
134 + $result = [
135 + 'plugin' => $exporter->get_plugin_slug(),
136 + 'plugin_name' => $exporter->get_plugin_name(),
137 + 'counts' => $counts,
138 + 'total' => array_sum($counts),
139 + ];
140 +
141 + set_transient(self::NATIVE_CACHE_KEY, $result, self::CACHE_TTL);
142 +
143 + return $result;
144 + }
145 +
146 + /**
98 147 * Clear the detection cache
99 148 *
100 149 * @return void
101 150 */
@@ -100,8 +149,9 @@
100 149 * @return void
101 150 */
102 151 public function clear_cache(): void {
103 152 delete_transient(self::CACHE_KEY);
153 + delete_transient(self::NATIVE_CACHE_KEY);
104 154 }
105 155
106 156 /**
107 157 * Detect a single plugin's data presence
@@ -116,10 +166,18 @@
116 166 // Only surface source plugins that are currently installed AND active.
117 167 // Migrating from a plugin the user no longer runs isn't actionable, so
118 168 // leftover data from a deactivated/uninstalled plugin is intentionally
119 169 // excluded from the migration screen.
170 + //
171 + // The one exception is Rank Math's FAQ / HowTo blocks. Those live in
172 + // post_content, and deactivating Rank Math is precisely what breaks
173 + // them: the blocks turn into "unsupported block" in the editor and lose
174 + // their schema on the front end. Hiding the only screen that can repair
175 + // them, at the exact moment they need repairing, left users with no way
176 + // out at all (#777) — so an inactive Rank Math stays listed whenever
177 + // convertible blocks remain, carrying that type and nothing else.
120 178 if (!$this->is_source_active($config['plugin_files'] ?? [])) {
121 - return null;
179 + return $this->detect_orphaned_blocks($slug, $config);
122 180 }
123 181
124 182 $counts = [];
125 183 $total = 0;
@@ -126,8 +184,11 @@
126 184
127 185 if ($slug === 'aioseo') {
128 186 // AIOSEO uses a custom table
129 187 $counts = $this->detect_aioseo();
188 + } elseif ($slug === 'squirrly') {
189 + // Squirrly uses a custom table too; the exporter classifies rows.
190 + $counts = $this->detect_squirrly();
130 191 } else {
131 192 // Standard postmeta-based plugins
132 193 $prefix = $config['meta_prefix'];
133 194
@@ -161,8 +222,18 @@
161 222 // when the exporter supports it (Rank Math Pro's redirects + 404
162 223 // Monitor and Yoast Premium's redirects were never offered).
163 224 $counts = array_merge($counts, $this->detect_redirect_data($slug));
164 225
226 + // FAQ / HowTo blocks, offered while Rank Math is still active too, so
227 + // the wizard converts them on the way out rather than leaving the user
228 + // to discover the breakage after deactivation.
229 + if ('rankmath' === $slug) {
230 + $block_count = Block_Converter::count_posts();
231 + if ($block_count > 0) {
232 + $counts[Block_Converter::TYPE] = $block_count;
233 + }
234 + }
235 +
165 236 // Check for settings
166 237 $has_settings = false;
167 238 foreach ($config['option_keys'] as $option_key) {
168 239 if (get_option($option_key, null) !== null) {
@@ -192,8 +263,39 @@
192 263 ];
193 264 }
194 265
195 266 /**
267 + * Detection result for a deactivated source that has left convertible
268 + * content blocks behind, or null when there is nothing to repair.
269 + *
270 + * Only the block type is reported. The plugin's postmeta and settings are
271 + * deliberately still withheld: migrating those from a plugin the site no
272 + * longer runs is the case the active-only rule exists to prevent, and it is
273 + * not what broke.
274 + *
275 + * @param string $slug Plugin slug.
276 + * @param array $config Plugin configuration.
277 + * @return array|null
278 + */
279 + private function detect_orphaned_blocks(string $slug, array $config): ?array {
280 + if ('rankmath' !== $slug) {
281 + return null;
282 + }
283 +
284 + $count = Block_Converter::count_posts();
285 + if ($count < 1) {
286 + return null;
287 + }
288 +
289 + return [
290 + 'plugin' => $slug,
291 + 'plugin_name' => $config['name'],
292 + 'counts' => [Block_Converter::TYPE => $count],
293 + 'total' => $count,
294 + ];
295 + }
296 +
297 + /**
196 298 * Whether any of a source plugin's known main files is active.
197 299 *
198 300 * Checks both single-site and network activation. A plugin that is merely
199 301 * installed but not active returns false.
@@ -261,13 +363,33 @@
261 363 if ($redirects > 0) {
262 364 $counts['redirections'] = $redirects;
263 365 }
264 366 }
367 + // Squirrly's Advanced Pack redirects are counted by the exporter in
368 + // detect_squirrly(), since only its `url` actions are redirects.
265 369
266 370 return $counts;
267 371 }
268 372
269 373 /**
374 + * Detect Squirrly SEO data (custom `qss` table + Advanced Pack redirects).
375 + *
376 + * Delegated to the exporter: the table mixes posts, terms, author
377 + * profiles and the homepage, and only the exporter knows how to tell
378 + * them apart from the serialized `post` column.
379 + *
380 + * @return array Counts array
381 + */
382 + private function detect_squirrly(): array {
383 + $exporter = new Squirrly_Exporter();
384 + $counts = $exporter->get_available_types();
385 + // Settings are added by the caller from option_keys.
386 + unset($counts['settings']);
387 +
388 + return $counts;
389 + }
390 +
391 + /**
270 392 * Detect AIOSEO custom table data
271 393 *
272 394 * @return array Counts array
273 395 */
@@ -282,8 +404,9 @@
282 404 $wpdb->prepare("SHOW TABLES LIKE %s", $table_name)
283 405 );
284 406
285 407 if ($table_exists) {
408 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix plus a literal, and every value is passed as a placeholder replacement.
286 409 $post_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
287 410 if ($post_count > 0) {
288 411 $counts['postmeta'] = $post_count;
289 412 }
@@ -295,8 +418,9 @@
295 418 $wpdb->prepare("SHOW TABLES LIKE %s", $redirects_table)
296 419 );
297 420
298 421 if ($redirects_exists) {
422 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix plus a literal, and every value is passed as a placeholder replacement.
299 423 $redirect_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$redirects_table}");
300 424 if ($redirect_count > 0) {
301 425 $counts['redirections'] = $redirect_count;
302 426 }