PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.10.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 1.0.1 All 51 releases
← All changes | includes/admin/importers/class-import-controller.php +134 -3 2.5.0 → 2.10.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'];
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', '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,9 @@
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
330 389 ];
331 390
332 391 $prefix = $prefix_map[$plugin] ?? null;
333 392
@@ -354,8 +413,9 @@
354 413 $usermeta_prefix_map = [
355 414 'yoast' => 'wpseo_',
356 415 'rankmath' => 'rank_math_',
357 416 'seopress' => '_seopress_',
417 + 'squirrly' => '_sq_',
358 418 ];
359 419 $usermeta_prefix = $usermeta_prefix_map[$plugin] ?? $prefix;
360 420 $deleted += (int) $wpdb->query(
361 421 $wpdb->prepare(
@@ -370,8 +430,9 @@
370 430 $option_keys_map = [
371 431 'yoast' => ['wpseo', 'wpseo_titles', 'wpseo_social', 'wpseo_taxonomy_meta'],
372 432 'rankmath' => ['rank-math-options-general', 'rank-math-options-titles', 'rank-math-options-sitemap', 'rank-math-options-instant-indexing'],
373 433 'seopress' => ['seopress_titles_option_name', 'seopress_social_option_name', 'seopress_advanced_option_name', 'seopress_xml_sitemap_option_name', 'seopress_instant_indexing_option_name'],
434 + 'squirrly' => ['sq_options'],
374 435 ];
375 436 foreach ($option_keys_map[$plugin] ?? [] as $option_name) {
376 437 if (delete_option($option_name)) {
377 438 $deleted++;
@@ -378,8 +439,26 @@
378 439 }
379 440 }
380 441 }
381 442
443 + if ($plugin === 'squirrly') {
444 + // Drop Squirrly's custom tables: per-URL SEO, Advanced Pack
445 + // redirects and their logs, reusable JSON-LD templates.
446 + foreach (['qss', 'qss_redirects', 'qss_redirects_logs', 'qss_jsonld'] as $suffix) {
447 + $table = $wpdb->prefix . $suffix;
448 + $table_exists = $wpdb->get_var(
449 + $wpdb->prepare("SHOW TABLES LIKE %s", $table)
450 + );
451 + if ($table_exists) {
452 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix plus a literal, and every value is passed as a placeholder replacement.
453 + $count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
454 + // 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.
455 + $wpdb->query("DROP TABLE {$table}");
456 + $deleted += $count;
457 + }
458 + }
459 + }
460 +
382 461 if ($plugin === 'aioseo') {
383 462 // Drop the AIOSEO custom tables the exporter reads (posts + Pro terms).
384 463 foreach (['aioseo_posts', 'aioseo_terms'] as $suffix) {
385 464 $table = $wpdb->prefix . $suffix;
@@ -423,8 +502,58 @@
423 502 ], 200);
424 503 }
425 504
426 505 /**
506 + * POST /import/content-blocks/restore: put converted posts back.
507 + *
508 + * Restores the pre-conversion `post_content` Block_Converter kept for posts
509 + * converted while revisions were disabled. A post edited since is reported
510 + * under `modified` and left alone unless `force` is set.
511 + *
512 + * Without `post_id` it works through one page of posts per call; while
513 + * `next_after` is non-null, call again with `after` set to it.
514 + *
515 + * @since 2.10.0
516 + *
517 + * @param \WP_REST_Request $request Request object
518 + * @return \WP_REST_Response
519 + */
520 + public function restore_content_blocks(\WP_REST_Request $request): \WP_REST_Response {
521 + $post_id = (int) $request->get_param('post_id');
522 + $force = (bool) $request->get_param('force');
523 + $limit = Block_Converter::chunk_size();
524 +
525 + $ids = $post_id > 0
526 + ? [$post_id]
527 + : Block_Converter::get_backup_post_ids((int) $request->get_param('after'), $limit);
528 +
529 + $report = [
530 + 'restored' => [],
531 + 'modified' => [],
532 + 'no_backup' => [],
533 + 'errors' => [],
534 + 'next_after' => null,
535 + ];
536 +
537 + foreach ($ids as $id) {
538 + $result = Block_Converter::restore_post($id, $force);
539 +
540 + if ('error' === $result['status']) {
541 + $report['errors'][] = ['post_id' => $id, 'message' => $result['message']];
542 + continue;
543 + }
544 +
545 + $report[$result['status']][] = $id;
546 + }
547 +
548 + if ($post_id < 1 && count($ids) === $limit) {
549 + $report['next_after'] = (int) end($ids);
550 + }
551 +
552 + return new \WP_REST_Response($report, 200);
553 + }
554 +
555 + /**
427 556 * DELETE /import/snapshot — Delete snapshot data from wp_options
428 557 *
429 558 * @param \WP_REST_Request $request Request object
430 559 * @return \WP_REST_Response
@@ -455,8 +584,10 @@
455 584 case 'seopress':
456 585 return new SEOPress_Exporter();
457 586 case 'aioseo':
458 587 return new AIOSEO_Exporter();
588 + case Squirrly_Exporter::SLUG:
589 + return new Squirrly_Exporter();
459 590 case Thinkrank_Exporter::SLUG:
460 591 return new Thinkrank_Exporter();
461 592 default:
462 593 return new \WP_Error('invalid_plugin', 'Unsupported plugin: ' . $plugin, ['status' => 400]);