← All changes
|
includes/admin/importers/class-import-controller.php
+234
-17
1.25.0
→
2.10.0
View file →
| @@ -35,21 +35,60 @@ | ||
| 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', 'squirrly']; | |
| 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', 'squirrly', '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 | 63 | */ |
| 49 | - 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 | + ]; | |
| 50 | 73 | |
| 51 | 74 | /** |
| 75 | + * Types the export and migrate endpoints accept. | |
| 76 | + * | |
| 77 | + * The fixed list above plus anything Pro registered through | |
| 78 | + * `thinkrank_export_types` — without this a Pro type would be exportable in | |
| 79 | + * principle and rejected at the route. | |
| 80 | + * | |
| 81 | + * @return string[] | |
| 82 | + */ | |
| 83 | + private function get_allowed_types(): array { | |
| 84 | + return array_values(array_unique(array_merge( | |
| 85 | + self::ALLOWED_TYPES, | |
| 86 | + Thinkrank_Exporter::get_exportable_types() | |
| 87 | + ))); | |
| 88 | + } | |
| 89 | + | |
| 90 | + /** | |
| 52 | 91 | * Register REST routes |
| 53 | 92 | * |
| 54 | 93 | * @return void |
| 55 | 94 | */ |
| @@ -58,8 +97,16 @@ | ||
| 58 | 97 | [ |
| 59 | 98 | 'methods' => \WP_REST_Server::READABLE, |
| 60 | 99 | 'callback' => [$this, 'detect'], |
| 61 | 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 | + ], | |
| 62 | 109 | ], |
| 63 | 110 | ]); |
| 64 | 111 | |
| 65 | 112 | register_rest_route($this->namespace, '/' . $this->rest_base . '/export', [ |
| @@ -96,9 +143,10 @@ | ||
| 96 | 143 | 'args' => [ |
| 97 | 144 | 'plugin' => [ |
| 98 | 145 | 'required' => true, |
| 99 | 146 | 'type' => 'string', |
| 100 | - 'enum' => self::ALLOWED_PLUGINS, | |
| 147 | + // Source plugins only — see SOURCE_PLUGINS. | |
| 148 | + 'enum' => self::SOURCE_PLUGINS, | |
| 101 | 149 | 'sanitize_callback' => 'sanitize_text_field', |
| 102 | 150 | ], |
| 103 | 151 | // Required to proceed while the snapshot still holds |
| 104 | 152 | // extended data with no migration path (see cleanup()). |
| @@ -110,8 +158,41 @@ | ||
| 110 | 158 | ], |
| 111 | 159 | ], |
| 112 | 160 | ]); |
| 113 | 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 | + | |
| 114 | 195 | register_rest_route($this->namespace, '/' . $this->rest_base . '/snapshot', [ |
| 115 | 196 | [ |
| 116 | 197 | 'methods' => \WP_REST_Server::DELETABLE, |
| 117 | 198 | 'callback' => [$this, 'delete_snapshot'], |
| @@ -137,20 +218,33 @@ | ||
| 137 | 218 | return current_user_can('manage_options'); |
| 138 | 219 | } |
| 139 | 220 | |
| 140 | 221 | /** |
| 141 | - * GET /import/detect — Detect source plugins and existing snapshots | |
| 222 | + * GET /import/detect — Detect source plugins, ThinkRank's own exportable | |
| 223 | + * data, and existing snapshots | |
| 142 | 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 | + * | |
| 143 | 229 | * @param \WP_REST_Request $request Request object |
| 144 | 230 | * @return \WP_REST_Response |
| 145 | 231 | */ |
| 146 | 232 | public function detect(\WP_REST_Request $request): \WP_REST_Response { |
| 147 | 233 | $detector = new Import_Detector(); |
| 234 | + | |
| 235 | + if ($request->get_param('refresh')) { | |
| 236 | + $detector->clear_cache(); | |
| 237 | + } | |
| 238 | + | |
| 148 | 239 | $detected = $detector->detect(); |
| 149 | 240 | $snapshots = Snapshot_Store::get_available_snapshots(); |
| 150 | 241 | |
| 151 | 242 | return new \WP_REST_Response([ |
| 152 | 243 | 'detected' => $detected, |
| 244 | + // ThinkRank's own exportable data, reported separately from the | |
| 245 | + // source plugins the user can migrate FROM. | |
| 246 | + 'native' => $detector->detect_native(), | |
| 153 | 247 | 'snapshots' => $snapshots, |
| 154 | 248 | ], 200); |
| 155 | 249 | } |
| 156 | 250 | |
| @@ -169,8 +263,15 @@ | ||
| 169 | 263 | if (is_wp_error($exporter)) { |
| 170 | 264 | return $exporter; |
| 171 | 265 | } |
| 172 | 266 | |
| 267 | + // Start a run from an empty slot. update_manifest() merges into whatever | |
| 268 | + // manifest is already there, so without this the file the user ends up | |
| 269 | + // downloading carries the union of this run and every run before it. | |
| 270 | + if ((bool) $request->get_param('reset')) { | |
| 271 | + Snapshot_Store::delete_snapshot($plugin); | |
| 272 | + } | |
| 273 | + | |
| 173 | 274 | $result = $exporter->export_chunk($type, $page); |
| 174 | 275 | |
| 175 | 276 | // If this type is complete and it's the last type, finalize |
| 176 | 277 | if (!$result['has_more'] && $request->get_param('is_last_type')) { |
| @@ -191,9 +292,10 @@ | ||
| 191 | 292 | return new \WP_REST_Response(['snapshots' => $snapshots], 200); |
| 192 | 293 | } |
| 193 | 294 | |
| 194 | 295 | /** |
| 195 | - * POST /import/migrate — Migrate a batch from snapshot to ThinkRank meta | |
| 296 | + * POST /import/migrate — Migrate a batch from snapshot to ThinkRank meta, | |
| 297 | + * or restore one from ThinkRank's own export | |
| 196 | 298 | * |
| 197 | 299 | * @param \WP_REST_Request $request Request object |
| 198 | 300 | * @return \WP_REST_Response |
| 199 | 301 | */ |
| @@ -200,11 +302,12 @@ | ||
| 200 | 302 | public function migrate(\WP_REST_Request $request): \WP_REST_Response { |
| 201 | 303 | $plugin = $request->get_param('plugin'); |
| 202 | 304 | $type = $request->get_param('type'); |
| 203 | 305 | $page = (int) $request->get_param('page'); |
| 306 | + $conflict = (string) $request->get_param('conflict'); | |
| 204 | 307 | |
| 205 | 308 | $migrator = new Snapshot_Migrator(); |
| 206 | - $result = $migrator->migrate_chunk($plugin, $type, $page); | |
| 309 | + $result = $migrator->migrate_chunk($plugin, $type, $page, $conflict); | |
| 207 | 310 | |
| 208 | 311 | // If migration is complete for all types, update manifest |
| 209 | 312 | if (!$result['has_more'] && $request->get_param('is_last_type')) { |
| 210 | 313 | $migrator->update_manifest_migration_info($plugin); |
| @@ -233,8 +336,19 @@ | ||
| 233 | 336 | $plugin = $request->get_param('plugin'); |
| 234 | 337 | $force = (bool) $request->get_param('force'); |
| 235 | 338 | $deleted = 0; |
| 236 | 339 | |
| 340 | + // Belt and braces on top of the route's SOURCE_PLUGINS enum: this | |
| 341 | + // endpoint deletes the SOURCE plugin's live meta and options, so | |
| 342 | + // pointing it at ThinkRank would delete the user's own SEO data. | |
| 343 | + if ($plugin === self::NATIVE_PLUGIN) { | |
| 344 | + return new \WP_Error( | |
| 345 | + 'thinkrank_cleanup_not_applicable', | |
| 346 | + __('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'), | |
| 347 | + ['status' => 400] | |
| 348 | + ); | |
| 349 | + } | |
| 350 | + | |
| 237 | 351 | // Gate: block while the snapshot holds preserved-but-unapplied extended |
| 238 | 352 | // data, unless the caller explicitly forces the cleanup. |
| 239 | 353 | if (!$force) { |
| 240 | 354 | $migrator = new Snapshot_Migrator(); |
| @@ -270,8 +384,9 @@ | ||
| 270 | 384 | 'yoast' => '_yoast_wpseo_', |
| 271 | 385 | 'rankmath' => 'rank_math_', |
| 272 | 386 | 'seopress' => '_seopress_', |
| 273 | 387 | 'aioseo' => null, // Custom table |
| 388 | + 'squirrly' => '_sq_', // Fallback meta only; the SEO is in the qss table below | |
| 274 | 389 | ]; |
| 275 | 390 | |
| 276 | 391 | $prefix = $prefix_map[$plugin] ?? null; |
| 277 | 392 | |
| @@ -298,8 +413,9 @@ | ||
| 298 | 413 | $usermeta_prefix_map = [ |
| 299 | 414 | 'yoast' => 'wpseo_', |
| 300 | 415 | 'rankmath' => 'rank_math_', |
| 301 | 416 | 'seopress' => '_seopress_', |
| 417 | + 'squirrly' => '_sq_', | |
| 302 | 418 | ]; |
| 303 | 419 | $usermeta_prefix = $usermeta_prefix_map[$plugin] ?? $prefix; |
| 304 | 420 | $deleted += (int) $wpdb->query( |
| 305 | 421 | $wpdb->prepare( |
| @@ -314,8 +430,9 @@ | ||
| 314 | 430 | $option_keys_map = [ |
| 315 | 431 | 'yoast' => ['wpseo', 'wpseo_titles', 'wpseo_social', 'wpseo_taxonomy_meta'], |
| 316 | 432 | 'rankmath' => ['rank-math-options-general', 'rank-math-options-titles', 'rank-math-options-sitemap', 'rank-math-options-instant-indexing'], |
| 317 | 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'], | |
| 318 | 435 | ]; |
| 319 | 436 | foreach ($option_keys_map[$plugin] ?? [] as $option_name) { |
| 320 | 437 | if (delete_option($option_name)) { |
| 321 | 438 | $deleted++; |
| @@ -322,8 +439,26 @@ | ||
| 322 | 439 | } |
| 323 | 440 | } |
| 324 | 441 | } |
| 325 | 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 | + | |
| 326 | 461 | if ($plugin === 'aioseo') { |
| 327 | 462 | // Drop the AIOSEO custom tables the exporter reads (posts + Pro terms). |
| 328 | 463 | foreach (['aioseo_posts', 'aioseo_terms'] as $suffix) { |
| 329 | 464 | $table = $wpdb->prefix . $suffix; |
| @@ -330,10 +465,11 @@ | ||
| 330 | 465 | $table_exists = $wpdb->get_var( |
| 331 | 466 | $wpdb->prepare("SHOW TABLES LIKE %s", $table) |
| 332 | 467 | ); |
| 333 | 468 | if ($table_exists) { |
| 469 | + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix plus a literal, and every value is passed as a placeholder replacement. | |
| 334 | 470 | $count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); |
| 335 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange | |
| 471 | + // 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. | |
| 336 | 472 | $wpdb->query("DROP TABLE {$table}"); |
| 337 | 473 | $deleted += $count; |
| 338 | 474 | } |
| 339 | 475 | } |
| @@ -366,8 +502,58 @@ | ||
| 366 | 502 | ], 200); |
| 367 | 503 | } |
| 368 | 504 | |
| 369 | 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 | + /** | |
| 370 | 556 | * DELETE /import/snapshot — Delete snapshot data from wp_options |
| 371 | 557 | * |
| 372 | 558 | * @param \WP_REST_Request $request Request object |
| 373 | 559 | * @return \WP_REST_Response |
| @@ -389,15 +575,24 @@ | ||
| 389 | 575 | * @param string $plugin Plugin slug |
| 390 | 576 | * @return Abstract_Plugin_Exporter|\WP_Error |
| 391 | 577 | */ |
| 392 | 578 | private function get_exporter(string $plugin) { |
| 393 | - return match ($plugin) { | |
| 394 | - 'yoast' => new Yoast_Exporter(), | |
| 395 | - 'rankmath' => new Rankmath_Exporter(), | |
| 396 | - 'seopress' => new SEOPress_Exporter(), | |
| 397 | - 'aioseo' => new AIOSEO_Exporter(), | |
| 398 | - default => new \WP_Error('invalid_plugin', 'Unsupported plugin: ' . $plugin, ['status' => 400]), | |
| 399 | - }; | |
| 579 | + switch ($plugin) { | |
| 580 | + case 'yoast': | |
| 581 | + return new Yoast_Exporter(); | |
| 582 | + case 'rankmath': | |
| 583 | + return new Rankmath_Exporter(); | |
| 584 | + case 'seopress': | |
| 585 | + return new SEOPress_Exporter(); | |
| 586 | + case 'aioseo': | |
| 587 | + return new AIOSEO_Exporter(); | |
| 588 | + case Squirrly_Exporter::SLUG: | |
| 589 | + return new Squirrly_Exporter(); | |
| 590 | + case Thinkrank_Exporter::SLUG: | |
| 591 | + return new Thinkrank_Exporter(); | |
| 592 | + default: | |
| 593 | + return new \WP_Error('invalid_plugin', 'Unsupported plugin: ' . $plugin, ['status' => 400]); | |
| 594 | + } | |
| 400 | 595 | } |
| 401 | 596 | |
| 402 | 597 | /** |
| 403 | 598 | * Get argument schema for export endpoint |
| @@ -414,9 +609,9 @@ | ||
| 414 | 609 | ], |
| 415 | 610 | 'type' => [ |
| 416 | 611 | 'required' => true, |
| 417 | 612 | 'type' => 'string', |
| 418 | - 'enum' => self::ALLOWED_TYPES, | |
| 613 | + 'enum' => $this->get_allowed_types(), | |
| 419 | 614 | 'sanitize_callback' => 'sanitize_text_field', |
| 420 | 615 | ], |
| 421 | 616 | 'page' => [ |
| 422 | 617 | 'required' => true, |
| @@ -428,8 +623,19 @@ | ||
| 428 | 623 | 'required' => false, |
| 429 | 624 | 'type' => 'boolean', |
| 430 | 625 | 'default' => false, |
| 431 | 626 | ], |
| 627 | + // Set on the first chunk of a run to discard whatever is already in | |
| 628 | + // the snapshot slot. Without it a run inherits the previous one's | |
| 629 | + // types: the manifest is merged into, never replaced, so a type the | |
| 630 | + // user deselected (or a file they uploaded and chose not to | |
| 631 | + // restore) stays in the snapshot and is streamed by | |
| 632 | + // /export/download, which sends every type the manifest lists. | |
| 633 | + 'reset' => [ | |
| 634 | + 'required' => false, | |
| 635 | + 'type' => 'boolean', | |
| 636 | + 'default' => false, | |
| 637 | + ], | |
| 432 | 638 | ]; |
| 433 | 639 | } |
| 434 | 640 | |
| 435 | 641 | /** |
| @@ -444,12 +650,23 @@ | ||
| 444 | 650 | 'type' => 'string', |
| 445 | 651 | 'enum' => self::ALLOWED_PLUGINS, |
| 446 | 652 | 'sanitize_callback' => 'sanitize_text_field', |
| 447 | 653 | ], |
| 654 | + // Defaults to skip, which is the safe answer for an import from | |
| 655 | + // another plugin: its data must never clobber something already | |
| 656 | + // set here. A restore from a ThinkRank backup passes overwrite — | |
| 657 | + // getting the saved values back is the entire point of it. | |
| 658 | + 'conflict' => [ | |
| 659 | + 'required' => false, | |
| 660 | + 'type' => 'string', | |
| 661 | + 'enum' => [Snapshot_Migrator::CONFLICT_SKIP, Snapshot_Migrator::CONFLICT_OVERWRITE], | |
| 662 | + 'default' => Snapshot_Migrator::CONFLICT_SKIP, | |
| 663 | + 'sanitize_callback' => 'sanitize_text_field', | |
| 664 | + ], | |
| 448 | 665 | 'type' => [ |
| 449 | 666 | 'required' => true, |
| 450 | 667 | 'type' => 'string', |
| 451 | - 'enum' => self::ALLOWED_TYPES, | |
| 668 | + 'enum' => $this->get_allowed_types(), | |
| 452 | 669 | 'sanitize_callback' => 'sanitize_text_field', |
| 453 | 670 | ], |
| 454 | 671 | 'page' => [ |
| 455 | 672 | 'required' => true, |