← All changes
|
includes/admin/importers/class-import-controller.php
+88
-8
2.0.2
→
2.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, |