← All changes
|
includes/admin/importers/class-import-detector.php
+150
-1
1.26.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,13 +375,46 @@ | ||
| 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. | |
| 265 | 381 | |
| 266 | 382 | return $counts; |
| 267 | 383 | } |
| 268 | 384 | |
| 269 | 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']); | |
| 412 | + | |
| 413 | + return $counts; | |
| 414 | + } | |
| 415 | + | |
| 416 | + /** | |
| 270 | 417 | * Detect AIOSEO custom table data |
| 271 | 418 | * |
| 272 | 419 | * @return array Counts array |
| 273 | 420 | */ |
| @@ -282,8 +429,9 @@ | ||
| 282 | 429 | $wpdb->prepare("SHOW TABLES LIKE %s", $table_name) |
| 283 | 430 | ); |
| 284 | 431 | |
| 285 | 432 | if ($table_exists) { |
| 433 | + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix plus a literal, and every value is passed as a placeholder replacement. | |
| 286 | 434 | $post_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}"); |
| 287 | 435 | if ($post_count > 0) { |
| 288 | 436 | $counts['postmeta'] = $post_count; |
| 289 | 437 | } |
| @@ -295,8 +443,9 @@ | ||
| 295 | 443 | $wpdb->prepare("SHOW TABLES LIKE %s", $redirects_table) |
| 296 | 444 | ); |
| 297 | 445 | |
| 298 | 446 | if ($redirects_exists) { |
| 447 | + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix plus a literal, and every value is passed as a placeholder replacement. | |
| 299 | 448 | $redirect_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$redirects_table}"); |
| 300 | 449 | if ($redirect_count > 0) { |
| 301 | 450 | $counts['redirections'] = $redirect_count; |
| 302 | 451 | } |