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-controller.php +179 -3 2.2.0 → 2.13.0 View file →
@@ -41,9 +41,9 @@
41 41 * Kept separate from ALLOWED_PLUGINS because cleanup() deletes the source
42 42 * plugin's live data: the native ThinkRank slug must never reach it, or the
43 43 * endpoint gains a path that wipes our own meta and options.
44 44 */
45 - private const SOURCE_PLUGINS = ['yoast', 'rankmath', 'seopress', 'aioseo'];
45 + private const SOURCE_PLUGINS = ['yoast', 'rankmath', 'seopress', 'aioseo', 'squirrly', 'slimseo'];
46 46
47 47 /**
48 48 * Snapshot slug for ThinkRank's own data (export / backup / restore).
49 49 */
@@ -52,9 +52,9 @@
52 52 /**
53 53 * Allowed plugin slugs for the snapshot endpoints (export, migrate,
54 54 * snapshot delete). Includes the native slug; cleanup uses SOURCE_PLUGINS.
55 55 */
56 - private const ALLOWED_PLUGINS = ['yoast', 'rankmath', 'seopress', 'aioseo', 'thinkrank'];
56 + private const ALLOWED_PLUGINS = ['yoast', 'rankmath', 'seopress', 'aioseo', 'squirrly', 'slimseo', 'thinkrank'];
57 57
58 58 /**
59 59 * Allowed export/migrate types. Must cover every type the exporters and
60 60 * the frontend workflow (useImportWorkflow.js EXPORT_TYPES) can send —
@@ -60,9 +60,17 @@
60 60 * the frontend workflow (useImportWorkflow.js EXPORT_TYPES) can send —
61 61 * 404_logs was missing, so Rank Math's 404 Monitor could never be
62 62 * exported or migrated through REST.
63 63 */
64 - private const ALLOWED_TYPES = ['postmeta', 'termmeta', 'usermeta', 'redirections', '404_logs', 'settings'];
64 + private const ALLOWED_TYPES = [
65 + 'postmeta',
66 + 'termmeta',
67 + 'usermeta',
68 + 'redirections',
69 + '404_logs',
70 + 'settings',
71 + Block_Converter::TYPE,
72 + ];
65 73
66 74 /**
67 75 * Types the export and migrate endpoints accept.
68 76 *
@@ -89,8 +97,16 @@
89 97 [
90 98 'methods' => \WP_REST_Server::READABLE,
91 99 'callback' => [$this, 'detect'],
92 100 'permission_callback' => [$this, 'check_permissions'],
101 + 'args' => [
102 + 'refresh' => [
103 + 'type' => 'boolean',
104 + 'default' => false,
105 + 'sanitize_callback' => 'rest_sanitize_boolean',
106 + 'description' => __('Discard the cached scan and re-read the database. Set by the Re-scan button; the screen\'s own load leaves it off so repeat navigation stays instant.', 'thinkrank'),
107 + ],
108 + ],
93 109 ],
94 110 ]);
95 111
96 112 register_rest_route($this->namespace, '/' . $this->rest_base . '/export', [
@@ -142,8 +158,41 @@
142 158 ],
143 159 ],
144 160 ]);
145 161
162 + // Undo for the Rank Math FAQ/HowTo block conversion on sites with
163 + // revisions disabled, where Block_Converter kept the original content
164 + // in post meta. With revisions on, the post's revision history is the
165 + // undo and this route has nothing to restore.
166 + register_rest_route($this->namespace, '/' . $this->rest_base . '/content-blocks/restore', [
167 + [
168 + 'methods' => \WP_REST_Server::CREATABLE,
169 + 'callback' => [$this, 'restore_content_blocks'],
170 + 'permission_callback' => [$this, 'check_permissions'],
171 + 'args' => [
172 + 'post_id' => [
173 + 'type' => 'integer',
174 + 'default' => 0,
175 + 'minimum' => 0,
176 + 'sanitize_callback' => 'absint',
177 + 'description' => __('Restore one post. Omit to restore every post with a backup, one page at a time.', 'thinkrank'),
178 + ],
179 + 'after' => [
180 + 'type' => 'integer',
181 + 'default' => 0,
182 + 'minimum' => 0,
183 + 'sanitize_callback' => 'absint',
184 + 'description' => __('Paging cursor: pass the previous response\'s next_after.', 'thinkrank'),
185 + ],
186 + 'force' => [
187 + 'type' => 'boolean',
188 + 'default' => false,
189 + 'description' => __('Restore posts that were edited after the conversion, discarding those edits.', 'thinkrank'),
190 + ],
191 + ],
192 + ],
193 + ]);
194 +
146 195 register_rest_route($this->namespace, '/' . $this->rest_base . '/snapshot', [
147 196 [
148 197 'methods' => \WP_REST_Server::DELETABLE,
149 198 'callback' => [$this, 'delete_snapshot'],
@@ -172,13 +221,22 @@
172 221 /**
173 222 * GET /import/detect — Detect source plugins, ThinkRank's own exportable
174 223 * data, and existing snapshots
175 224 *
225 + * `refresh` drops the hour-long detection transient before scanning. Without
226 + * it the Re-scan button could not do the one thing it exists for: pick up a
227 + * source plugin the user just installed, activated or added data to.
228 + *
176 229 * @param \WP_REST_Request $request Request object
177 230 * @return \WP_REST_Response
178 231 */
179 232 public function detect(\WP_REST_Request $request): \WP_REST_Response {
180 233 $detector = new Import_Detector();
234 +
235 + if ($request->get_param('refresh')) {
236 + $detector->clear_cache();
237 + }
238 +
181 239 $detected = $detector->detect();
182 240 $snapshots = Snapshot_Store::get_available_snapshots();
183 241
184 242 return new \WP_REST_Response([
@@ -326,8 +384,12 @@
326 384 'yoast' => '_yoast_wpseo_',
327 385 'rankmath' => 'rank_math_',
328 386 'seopress' => '_seopress_',
329 387 'aioseo' => null, // Custom table
388 + 'squirrly' => '_sq_', // Fallback meta only; the SEO is in the qss table below
389 + // Slim SEO is cleaned by exact key below: a `slim_seo` prefix also
390 + // matches Slim SEO Pro's and its add-ons' own keys (#886).
391 + 'slimseo' => null,
330 392 ];
331 393
332 394 $prefix = $prefix_map[$plugin] ?? null;
333 395
@@ -354,8 +416,9 @@
354 416 $usermeta_prefix_map = [
355 417 'yoast' => 'wpseo_',
356 418 'rankmath' => 'rank_math_',
357 419 'seopress' => '_seopress_',
420 + 'squirrly' => '_sq_',
358 421 ];
359 422 $usermeta_prefix = $usermeta_prefix_map[$plugin] ?? $prefix;
360 423 $deleted += (int) $wpdb->query(
361 424 $wpdb->prepare(
@@ -370,8 +433,9 @@
370 433 $option_keys_map = [
371 434 'yoast' => ['wpseo', 'wpseo_titles', 'wpseo_social', 'wpseo_taxonomy_meta'],
372 435 'rankmath' => ['rank-math-options-general', 'rank-math-options-titles', 'rank-math-options-sitemap', 'rank-math-options-instant-indexing'],
373 436 'seopress' => ['seopress_titles_option_name', 'seopress_social_option_name', 'seopress_advanced_option_name', 'seopress_xml_sitemap_option_name', 'seopress_instant_indexing_option_name'],
437 + 'squirrly' => ['sq_options'],
374 438 ];
375 439 foreach ($option_keys_map[$plugin] ?? [] as $option_name) {
376 440 if (delete_option($option_name)) {
377 441 $deleted++;
@@ -378,8 +442,66 @@
378 442 }
379 443 }
380 444 }
381 445
446 + if ($plugin === 'slimseo') {
447 + // Exactly the keys the exporter reads. A `slim_seo%` prefix would
448 + // also take data nothing imported (Slim SEO Link Manager's and
449 + // Auto Link's settings), and those stay with their plugin.
450 + $deleted += (int) $wpdb->query(
451 + $wpdb->prepare(
452 + "DELETE FROM {$wpdb->postmeta} WHERE meta_key IN (%s, %s, %s, %s, %s) OR meta_key LIKE %s",
453 + Slim_SEO_Exporter::META_KEY,
454 + Slim_SEO_Exporter::PRO_META_KEY,
455 + Slim_SEO_Exporter::SCHEMA_META_KEY,
456 + Slim_SEO_Exporter::ALLOW_GLOBAL_META,
457 + Slim_SEO_Exporter::OLD_PERMALINK_META,
458 + $wpdb->esc_like(Slim_SEO_Exporter::PRIMARY_TERM_PREFIX) . '%'
459 + )
460 + );
461 + $deleted += (int) $wpdb->query(
462 + $wpdb->prepare("DELETE FROM {$wpdb->termmeta} WHERE meta_key = %s", Slim_SEO_Exporter::META_KEY)
463 + );
464 + $options = [
465 + Slim_SEO_Exporter::OPTION,
466 + Slim_SEO_Exporter::REDIRECTS_OPTION,
467 + Slim_SEO_Exporter::PRO_OPTION,
468 + Slim_SEO_Exporter::SCHEMAS_OPTION,
469 + 'slim_seo_db_version',
470 + 'ss_redirection_db_version',
471 + ];
472 + foreach ($options as $option_name) {
473 + if (delete_option($option_name)) {
474 + $deleted++;
475 + }
476 + }
477 + $table = $wpdb->prefix . Slim_SEO_Exporter::LOG_404_TABLE;
478 + if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table))) {
479 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix plus a literal.
480 + $deleted += (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
481 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix plus a literal.
482 + $wpdb->query("DROP TABLE {$table}");
483 + }
484 + }
485 +
486 + if ($plugin === 'squirrly') {
487 + // Drop Squirrly's custom tables: per-URL SEO, Advanced Pack
488 + // redirects and their logs, reusable JSON-LD templates.
489 + foreach (['qss', 'qss_redirects', 'qss_redirects_logs', 'qss_jsonld'] as $suffix) {
490 + $table = $wpdb->prefix . $suffix;
491 + $table_exists = $wpdb->get_var(
492 + $wpdb->prepare("SHOW TABLES LIKE %s", $table)
493 + );
494 + if ($table_exists) {
495 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix plus a literal, and every value is passed as a placeholder replacement.
496 + $count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
497 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix plus a literal, and every value is passed as a placeholder replacement.
498 + $wpdb->query("DROP TABLE {$table}");
499 + $deleted += $count;
500 + }
501 + }
502 + }
503 +
382 504 if ($plugin === 'aioseo') {
383 505 // Drop the AIOSEO custom tables the exporter reads (posts + Pro terms).
384 506 foreach (['aioseo_posts', 'aioseo_terms'] as $suffix) {
385 507 $table = $wpdb->prefix . $suffix;
@@ -423,8 +545,58 @@
423 545 ], 200);
424 546 }
425 547
426 548 /**
549 + * POST /import/content-blocks/restore: put converted posts back.
550 + *
551 + * Restores the pre-conversion `post_content` Block_Converter kept for posts
552 + * converted while revisions were disabled. A post edited since is reported
553 + * under `modified` and left alone unless `force` is set.
554 + *
555 + * Without `post_id` it works through one page of posts per call; while
556 + * `next_after` is non-null, call again with `after` set to it.
557 + *
558 + * @since 2.10.0
559 + *
560 + * @param \WP_REST_Request $request Request object
561 + * @return \WP_REST_Response
562 + */
563 + public function restore_content_blocks(\WP_REST_Request $request): \WP_REST_Response {
564 + $post_id = (int) $request->get_param('post_id');
565 + $force = (bool) $request->get_param('force');
566 + $limit = Block_Converter::chunk_size();
567 +
568 + $ids = $post_id > 0
569 + ? [$post_id]
570 + : Block_Converter::get_backup_post_ids((int) $request->get_param('after'), $limit);
571 +
572 + $report = [
573 + 'restored' => [],
574 + 'modified' => [],
575 + 'no_backup' => [],
576 + 'errors' => [],
577 + 'next_after' => null,
578 + ];
579 +
580 + foreach ($ids as $id) {
581 + $result = Block_Converter::restore_post($id, $force);
582 +
583 + if ('error' === $result['status']) {
584 + $report['errors'][] = ['post_id' => $id, 'message' => $result['message']];
585 + continue;
586 + }
587 +
588 + $report[$result['status']][] = $id;
589 + }
590 +
591 + if ($post_id < 1 && count($ids) === $limit) {
592 + $report['next_after'] = (int) end($ids);
593 + }
594 +
595 + return new \WP_REST_Response($report, 200);
596 + }
597 +
598 + /**
427 599 * DELETE /import/snapshot — Delete snapshot data from wp_options
428 600 *
429 601 * @param \WP_REST_Request $request Request object
430 602 * @return \WP_REST_Response
@@ -455,8 +627,12 @@
455 627 case 'seopress':
456 628 return new SEOPress_Exporter();
457 629 case 'aioseo':
458 630 return new AIOSEO_Exporter();
631 + case Squirrly_Exporter::SLUG:
632 + return new Squirrly_Exporter();
633 + case Slim_SEO_Exporter::SLUG:
634 + return new Slim_SEO_Exporter();
459 635 case Thinkrank_Exporter::SLUG:
460 636 return new Thinkrank_Exporter();
461 637 default:
462 638 return new \WP_Error('invalid_plugin', 'Unsupported plugin: ' . $plugin, ['status' => 400]);