PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.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 1.0.2 1.1.0 All 49 releases
← All changes | includes/admin/importers/class-import-controller.php +88 -8 2.0.22.7.0 View file →
@@ -35,13 +35,28 @@
35 35 */
36 36 protected $rest_base = 'import';
37 37
38 38 /**
39 - * Allowed plugin slugs
39 + * Source plugins ThinkRank can migrate FROM.
40 + *
41 + * Kept separate from ALLOWED_PLUGINS because cleanup() deletes the source
42 + * plugin's live data: the native ThinkRank slug must never reach it, or the
43 + * endpoint gains a path that wipes our own meta and options.
40 44 */
41 - private const ALLOWED_PLUGINS = ['yoast', 'rankmath', 'seopress', 'aioseo'];
45 + private const SOURCE_PLUGINS = ['yoast', 'rankmath', 'seopress', 'aioseo'];
42 46
43 47 /**
48 + * Snapshot slug for ThinkRank's own data (export / backup / restore).
49 + */
50 + private const NATIVE_PLUGIN = Thinkrank_Exporter::SLUG;
51 +
52 + /**
53 + * Allowed plugin slugs for the snapshot endpoints (export, migrate,
54 + * snapshot delete). Includes the native slug; cleanup uses SOURCE_PLUGINS.
55 + */
56 + private const ALLOWED_PLUGINS = ['yoast', 'rankmath', 'seopress', 'aioseo', 'thinkrank'];
57 +
58 + /**
44 59 * Allowed export/migrate types. Must cover every type the exporters and
45 60 * the frontend workflow (useImportWorkflow.js EXPORT_TYPES) can send —
46 61 * 404_logs was missing, so Rank Math's 404 Monitor could never be
47 62 * exported or migrated through REST.
@@ -48,8 +63,24 @@
48 63 */
49 64 private const ALLOWED_TYPES = ['postmeta', 'termmeta', 'usermeta', 'redirections', '404_logs', 'settings'];
50 65
51 66 /**
67 + * Types the export and migrate endpoints accept.
68 + *
69 + * The fixed list above plus anything Pro registered through
70 + * `thinkrank_export_types` — without this a Pro type would be exportable in
71 + * principle and rejected at the route.
72 + *
73 + * @return string[]
74 + */
75 + private function get_allowed_types(): array {
76 + return array_values(array_unique(array_merge(
77 + self::ALLOWED_TYPES,
78 + Thinkrank_Exporter::get_exportable_types()
79 + )));
80 + }
81 +
82 + /**
52 83 * Register REST routes
53 84 *
54 85 * @return void
55 86 */
@@ -96,9 +127,10 @@
96 127 'args' => [
97 128 'plugin' => [
98 129 'required' => true,
99 130 'type' => 'string',
100 - 'enum' => self::ALLOWED_PLUGINS,
131 + // Source plugins only — see SOURCE_PLUGINS.
132 + 'enum' => self::SOURCE_PLUGINS,
101 133 'sanitize_callback' => 'sanitize_text_field',
102 134 ],
103 135 // Required to proceed while the snapshot still holds
104 136 // extended data with no migration path (see cleanup()).
@@ -137,9 +169,10 @@
137 169 return current_user_can('manage_options');
138 170 }
139 171
140 172 /**
141 - * GET /import/detect — Detect source plugins and existing snapshots
173 + * GET /import/detect — Detect source plugins, ThinkRank's own exportable
174 + * data, and existing snapshots
142 175 *
143 176 * @param \WP_REST_Request $request Request object
144 177 * @return \WP_REST_Response
145 178 */
@@ -149,8 +182,11 @@
149 182 $snapshots = Snapshot_Store::get_available_snapshots();
150 183
151 184 return new \WP_REST_Response([
152 185 'detected' => $detected,
186 + // ThinkRank's own exportable data, reported separately from the
187 + // source plugins the user can migrate FROM.
188 + 'native' => $detector->detect_native(),
153 189 'snapshots' => $snapshots,
154 190 ], 200);
155 191 }
156 192
@@ -169,8 +205,15 @@
169 205 if (is_wp_error($exporter)) {
170 206 return $exporter;
171 207 }
172 208
209 + // Start a run from an empty slot. update_manifest() merges into whatever
210 + // manifest is already there, so without this the file the user ends up
211 + // downloading carries the union of this run and every run before it.
212 + if ((bool) $request->get_param('reset')) {
213 + Snapshot_Store::delete_snapshot($plugin);
214 + }
215 +
173 216 $result = $exporter->export_chunk($type, $page);
174 217
175 218 // If this type is complete and it's the last type, finalize
176 219 if (!$result['has_more'] && $request->get_param('is_last_type')) {
@@ -191,9 +234,10 @@
191 234 return new \WP_REST_Response(['snapshots' => $snapshots], 200);
192 235 }
193 236
194 237 /**
195 - * POST /import/migrate — Migrate a batch from snapshot to ThinkRank meta
238 + * POST /import/migrate — Migrate a batch from snapshot to ThinkRank meta,
239 + * or restore one from ThinkRank's own export
196 240 *
197 241 * @param \WP_REST_Request $request Request object
198 242 * @return \WP_REST_Response
199 243 */
@@ -200,11 +244,12 @@
200 244 public function migrate(\WP_REST_Request $request): \WP_REST_Response {
201 245 $plugin = $request->get_param('plugin');
202 246 $type = $request->get_param('type');
203 247 $page = (int) $request->get_param('page');
248 + $conflict = (string) $request->get_param('conflict');
204 249
205 250 $migrator = new Snapshot_Migrator();
206 - $result = $migrator->migrate_chunk($plugin, $type, $page);
251 + $result = $migrator->migrate_chunk($plugin, $type, $page, $conflict);
207 252
208 253 // If migration is complete for all types, update manifest
209 254 if (!$result['has_more'] && $request->get_param('is_last_type')) {
210 255 $migrator->update_manifest_migration_info($plugin);
@@ -233,8 +278,19 @@
233 278 $plugin = $request->get_param('plugin');
234 279 $force = (bool) $request->get_param('force');
235 280 $deleted = 0;
236 281
282 + // Belt and braces on top of the route's SOURCE_PLUGINS enum: this
283 + // endpoint deletes the SOURCE plugin's live meta and options, so
284 + // pointing it at ThinkRank would delete the user's own SEO data.
285 + if ($plugin === self::NATIVE_PLUGIN) {
286 + return new \WP_Error(
287 + 'thinkrank_cleanup_not_applicable',
288 + __('Cleanup removes a source plugin\'s data and does not apply to ThinkRank\'s own export. Use DELETE /import/snapshot to discard the snapshot.', 'thinkrank'),
289 + ['status' => 400]
290 + );
291 + }
292 +
237 293 // Gate: block while the snapshot holds preserved-but-unapplied extended
238 294 // data, unless the caller explicitly forces the cleanup.
239 295 if (!$force) {
240 296 $migrator = new Snapshot_Migrator();
@@ -399,8 +455,10 @@
399 455 case 'seopress':
400 456 return new SEOPress_Exporter();
401 457 case 'aioseo':
402 458 return new AIOSEO_Exporter();
459 + case Thinkrank_Exporter::SLUG:
460 + return new Thinkrank_Exporter();
403 461 default:
404 462 return new \WP_Error('invalid_plugin', 'Unsupported plugin: ' . $plugin, ['status' => 400]);
405 463 }
406 464 }
@@ -420,9 +478,9 @@
420 478 ],
421 479 'type' => [
422 480 'required' => true,
423 481 'type' => 'string',
424 - 'enum' => self::ALLOWED_TYPES,
482 + 'enum' => $this->get_allowed_types(),
425 483 'sanitize_callback' => 'sanitize_text_field',
426 484 ],
427 485 'page' => [
428 486 'required' => true,
@@ -434,8 +492,19 @@
434 492 'required' => false,
435 493 'type' => 'boolean',
436 494 'default' => false,
437 495 ],
496 + // Set on the first chunk of a run to discard whatever is already in
497 + // the snapshot slot. Without it a run inherits the previous one's
498 + // types: the manifest is merged into, never replaced, so a type the
499 + // user deselected (or a file they uploaded and chose not to
500 + // restore) stays in the snapshot and is streamed by
501 + // /export/download, which sends every type the manifest lists.
502 + 'reset' => [
503 + 'required' => false,
504 + 'type' => 'boolean',
505 + 'default' => false,
506 + ],
438 507 ];
439 508 }
440 509
441 510 /**
@@ -450,12 +519,23 @@
450 519 'type' => 'string',
451 520 'enum' => self::ALLOWED_PLUGINS,
452 521 'sanitize_callback' => 'sanitize_text_field',
453 522 ],
523 + // Defaults to skip, which is the safe answer for an import from
524 + // another plugin: its data must never clobber something already
525 + // set here. A restore from a ThinkRank backup passes overwrite —
526 + // getting the saved values back is the entire point of it.
527 + 'conflict' => [
528 + 'required' => false,
529 + 'type' => 'string',
530 + 'enum' => [Snapshot_Migrator::CONFLICT_SKIP, Snapshot_Migrator::CONFLICT_OVERWRITE],
531 + 'default' => Snapshot_Migrator::CONFLICT_SKIP,
532 + 'sanitize_callback' => 'sanitize_text_field',
533 + ],
454 534 'type' => [
455 535 'required' => true,
456 536 'type' => 'string',
457 - 'enum' => self::ALLOWED_TYPES,
537 + 'enum' => $this->get_allowed_types(),
458 538 'sanitize_callback' => 'sanitize_text_field',
459 539 ],
460 540 'page' => [
461 541 'required' => true,