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
thinkrank / includes / admin / importers / class-import-controller.php

class-import-controller.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.10.0, at includes/admin/importers/class-import-controller.php

685 lines 26.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Import Controller
5 *
6 * REST API endpoints for the two-phase SEO data import system.
7 * 6 endpoints under thinkrank/v1/import.
8 *
9 * @package ThinkRank\Admin\Importers
10 * @since 2.0.0
11 */
12
13 declare(strict_types=1);
14
15 namespace ThinkRank\Admin\Importers;
16
17 if (!defined('ABSPATH')) {
18 exit;
19 }
20
21 /**
22 * Import Controller Class
23 *
24 * @since 2.0.0
25 */
26 class Import_Controller extends \WP_REST_Controller {
27
28 /**
29 * @var string
30 */
31 protected $namespace = 'thinkrank/v1';
32
33 /**
34 * @var string
35 */
36 protected $rest_base = 'import';
37
38 /**
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.
44 */
45 private const SOURCE_PLUGINS = ['yoast', 'rankmath', 'seopress', 'aioseo', 'squirrly'];
46
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 /**
59 * Allowed export/migrate types. Must cover every type the exporters and
60 * the frontend workflow (useImportWorkflow.js EXPORT_TYPES) can send —
61 * 404_logs was missing, so Rank Math's 404 Monitor could never be
62 * exported or migrated through REST.
63 */
64 private const ALLOWED_TYPES = [
65 'postmeta',
66 'termmeta',
67 'usermeta',
68 'redirections',
69 '404_logs',
70 'settings',
71 Block_Converter::TYPE,
72 ];
73
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 /**
91 * Register REST routes
92 *
93 * @return void
94 */
95 public function register_routes(): void {
96 register_rest_route($this->namespace, '/' . $this->rest_base . '/detect', [
97 [
98 'methods' => \WP_REST_Server::READABLE,
99 'callback' => [$this, 'detect'],
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 ],
109 ],
110 ]);
111
112 register_rest_route($this->namespace, '/' . $this->rest_base . '/export', [
113 [
114 'methods' => \WP_REST_Server::CREATABLE,
115 'callback' => [$this, 'export'],
116 'permission_callback' => [$this, 'check_permissions'],
117 'args' => $this->get_export_args(),
118 ],
119 ]);
120
121 register_rest_route($this->namespace, '/' . $this->rest_base . '/snapshots', [
122 [
123 'methods' => \WP_REST_Server::READABLE,
124 'callback' => [$this, 'get_snapshots'],
125 'permission_callback' => [$this, 'check_permissions'],
126 ],
127 ]);
128
129 register_rest_route($this->namespace, '/' . $this->rest_base . '/migrate', [
130 [
131 'methods' => \WP_REST_Server::CREATABLE,
132 'callback' => [$this, 'migrate'],
133 'permission_callback' => [$this, 'check_permissions'],
134 'args' => $this->get_migrate_args(),
135 ],
136 ]);
137
138 register_rest_route($this->namespace, '/' . $this->rest_base . '/cleanup', [
139 [
140 'methods' => \WP_REST_Server::CREATABLE,
141 'callback' => [$this, 'cleanup'],
142 'permission_callback' => [$this, 'check_permissions'],
143 'args' => [
144 'plugin' => [
145 'required' => true,
146 'type' => 'string',
147 // Source plugins only — see SOURCE_PLUGINS.
148 'enum' => self::SOURCE_PLUGINS,
149 'sanitize_callback' => 'sanitize_text_field',
150 ],
151 // Required to proceed while the snapshot still holds
152 // extended data with no migration path (see cleanup()).
153 'force' => [
154 'required' => false,
155 'type' => 'boolean',
156 'default' => false,
157 ],
158 ],
159 ],
160 ]);
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
195 register_rest_route($this->namespace, '/' . $this->rest_base . '/snapshot', [
196 [
197 'methods' => \WP_REST_Server::DELETABLE,
198 'callback' => [$this, 'delete_snapshot'],
199 'permission_callback' => [$this, 'check_permissions'],
200 'args' => [
201 'plugin' => [
202 'required' => true,
203 'type' => 'string',
204 'enum' => self::ALLOWED_PLUGINS,
205 'sanitize_callback' => 'sanitize_text_field',
206 ],
207 ],
208 ],
209 ]);
210 }
211
212 /**
213 * Permission check — manage_options required
214 *
215 * @return bool
216 */
217 public function check_permissions(): bool {
218 return current_user_can('manage_options');
219 }
220
221 /**
222 * GET /import/detect — Detect source plugins, ThinkRank's own exportable
223 * data, and existing snapshots
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 *
229 * @param \WP_REST_Request $request Request object
230 * @return \WP_REST_Response
231 */
232 public function detect(\WP_REST_Request $request): \WP_REST_Response {
233 $detector = new Import_Detector();
234
235 if ($request->get_param('refresh')) {
236 $detector->clear_cache();
237 }
238
239 $detected = $detector->detect();
240 $snapshots = Snapshot_Store::get_available_snapshots();
241
242 return new \WP_REST_Response([
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(),
247 'snapshots' => $snapshots,
248 ], 200);
249 }
250
251 /**
252 * POST /import/export — Export a batch of source data to snapshot
253 *
254 * @param \WP_REST_Request $request Request object
255 * @return \WP_REST_Response|\WP_Error
256 */
257 public function export(\WP_REST_Request $request) {
258 $plugin = $request->get_param('plugin');
259 $type = $request->get_param('type');
260 $page = (int) $request->get_param('page');
261
262 $exporter = $this->get_exporter($plugin);
263 if (is_wp_error($exporter)) {
264 return $exporter;
265 }
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
274 $result = $exporter->export_chunk($type, $page);
275
276 // If this type is complete and it's the last type, finalize
277 if (!$result['has_more'] && $request->get_param('is_last_type')) {
278 $exporter->finalize_export();
279 }
280
281 return new \WP_REST_Response($result, 200);
282 }
283
284 /**
285 * GET /import/snapshots — List existing snapshots
286 *
287 * @param \WP_REST_Request $request Request object
288 * @return \WP_REST_Response
289 */
290 public function get_snapshots(\WP_REST_Request $request): \WP_REST_Response {
291 $snapshots = Snapshot_Store::get_available_snapshots();
292 return new \WP_REST_Response(['snapshots' => $snapshots], 200);
293 }
294
295 /**
296 * POST /import/migrate — Migrate a batch from snapshot to ThinkRank meta,
297 * or restore one from ThinkRank's own export
298 *
299 * @param \WP_REST_Request $request Request object
300 * @return \WP_REST_Response
301 */
302 public function migrate(\WP_REST_Request $request): \WP_REST_Response {
303 $plugin = $request->get_param('plugin');
304 $type = $request->get_param('type');
305 $page = (int) $request->get_param('page');
306 $conflict = (string) $request->get_param('conflict');
307
308 $migrator = new Snapshot_Migrator();
309 $result = $migrator->migrate_chunk($plugin, $type, $page, $conflict);
310
311 // If migration is complete for all types, update manifest
312 if (!$result['has_more'] && $request->get_param('is_last_type')) {
313 $migrator->update_manifest_migration_info($plugin);
314 }
315
316 return new \WP_REST_Response($result, 200);
317 }
318
319 /**
320 * POST /import/cleanup — Remove source plugin meta from database
321 *
322 * Deletes SOURCE plugin data only — the ThinkRank snapshot is never touched
323 * by this endpoint (use DELETE /import/snapshot for that). Because the
324 * snapshot may still hold extended data ThinkRank cannot apply yet (e.g.
325 * redirections, owed to the Pro Redirections feature), cleanup is gated:
326 * while such buckets exist the request is rejected with HTTP 409 unless
327 * force=true is passed, so the user explicitly acknowledges that the
328 * snapshot becomes the only copy of that data.
329 *
330 * @param \WP_REST_Request $request Request object
331 * @return \WP_REST_Response|\WP_Error
332 */
333 public function cleanup(\WP_REST_Request $request) {
334 global $wpdb;
335
336 $plugin = $request->get_param('plugin');
337 $force = (bool) $request->get_param('force');
338 $deleted = 0;
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
351 // Gate: block while the snapshot holds preserved-but-unapplied extended
352 // data, unless the caller explicitly forces the cleanup.
353 if (!$force) {
354 $migrator = new Snapshot_Migrator();
355 $unmigrated = $migrator->get_unmigrated_extended_buckets($plugin);
356
357 if (!empty($unmigrated)) {
358 $labels = array_map(
359 static function (array $bucket): string {
360 return $bucket['count'] > 1
361 ? sprintf('%s (%d)', $bucket['label'], $bucket['count'])
362 : (string) $bucket['label'];
363 },
364 $unmigrated
365 );
366
367 return new \WP_Error(
368 'thinkrank_cleanup_blocked',
369 sprintf(
370 /* translators: %s: comma-separated list of unapplied data buckets. */
371 __('The snapshot still holds data ThinkRank has not applied yet: %s. It stays preserved in the snapshot (cleanup never deletes the snapshot), but the source plugin\'s copy will be removed. Pass force=true to proceed.', 'thinkrank'),
372 implode(', ', $labels)
373 ),
374 [
375 'status' => 409,
376 'preserved' => $unmigrated,
377 'requires_force' => true,
378 ]
379 );
380 }
381 }
382
383 $prefix_map = [
384 'yoast' => '_yoast_wpseo_',
385 'rankmath' => 'rank_math_',
386 'seopress' => '_seopress_',
387 'aioseo' => null, // Custom table
388 'squirrly' => '_sq_', // Fallback meta only; the SEO is in the qss table below
389 ];
390
391 $prefix = $prefix_map[$plugin] ?? null;
392
393 if ($prefix) {
394 // Delete from postmeta
395 $deleted += (int) $wpdb->query(
396 $wpdb->prepare(
397 "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE %s",
398 $wpdb->esc_like($prefix) . '%'
399 )
400 );
401
402 // Delete from termmeta
403 $deleted += (int) $wpdb->query(
404 $wpdb->prepare(
405 "DELETE FROM {$wpdb->termmeta} WHERE meta_key LIKE %s",
406 $wpdb->esc_like($prefix) . '%'
407 )
408 );
409
410 // Delete author-level SEO meta from usermeta. Yoast keys user meta
411 // under a different prefix than its post meta, so map it explicitly;
412 // the others reuse their post-meta prefix.
413 $usermeta_prefix_map = [
414 'yoast' => 'wpseo_',
415 'rankmath' => 'rank_math_',
416 'seopress' => '_seopress_',
417 'squirrly' => '_sq_',
418 ];
419 $usermeta_prefix = $usermeta_prefix_map[$plugin] ?? $prefix;
420 $deleted += (int) $wpdb->query(
421 $wpdb->prepare(
422 "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE %s",
423 $wpdb->esc_like($usermeta_prefix) . '%'
424 )
425 );
426
427 // Delete the source plugin's option rows — the exact options each
428 // exporter reads, so the site-level settings we migrated are removed
429 // too rather than left orphaned.
430 $option_keys_map = [
431 'yoast' => ['wpseo', 'wpseo_titles', 'wpseo_social', 'wpseo_taxonomy_meta'],
432 'rankmath' => ['rank-math-options-general', 'rank-math-options-titles', 'rank-math-options-sitemap', 'rank-math-options-instant-indexing'],
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'],
435 ];
436 foreach ($option_keys_map[$plugin] ?? [] as $option_name) {
437 if (delete_option($option_name)) {
438 $deleted++;
439 }
440 }
441 }
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
461 if ($plugin === 'aioseo') {
462 // Drop the AIOSEO custom tables the exporter reads (posts + Pro terms).
463 foreach (['aioseo_posts', 'aioseo_terms'] as $suffix) {
464 $table = $wpdb->prefix . $suffix;
465 $table_exists = $wpdb->get_var(
466 $wpdb->prepare("SHOW TABLES LIKE %s", $table)
467 );
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.
470 $count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
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.
472 $wpdb->query("DROP TABLE {$table}");
473 $deleted += $count;
474 }
475 }
476
477 // Legacy term meta (aioseo_*-prefixed) some versions used.
478 $deleted += (int) $wpdb->query(
479 $wpdb->prepare(
480 "DELETE FROM {$wpdb->termmeta} WHERE meta_key LIKE %s",
481 $wpdb->esc_like('aioseo_') . '%'
482 )
483 );
484
485 // The option blobs the exporter reads (settings, dynamic per-type
486 // templates, Pro add-on settings).
487 foreach (['aioseo_options', 'aioseo_options_dynamic', 'aioseo_options_pro'] as $option_name) {
488 if (delete_option($option_name)) {
489 $deleted++;
490 }
491 }
492 }
493
494 // Clear detection cache
495 $detector = new Import_Detector();
496 $detector->clear_cache();
497
498 return new \WP_REST_Response([
499 'status' => 'complete',
500 'message' => sprintf('Deleted %d source data entries for %s', $deleted, $plugin),
501 'deleted' => $deleted,
502 ], 200);
503 }
504
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 /**
556 * DELETE /import/snapshot — Delete snapshot data from wp_options
557 *
558 * @param \WP_REST_Request $request Request object
559 * @return \WP_REST_Response
560 */
561 public function delete_snapshot(\WP_REST_Request $request): \WP_REST_Response {
562 $plugin = $request->get_param('plugin');
563 $deleted = Snapshot_Store::delete_snapshot($plugin);
564
565 return new \WP_REST_Response([
566 'status' => 'complete',
567 'message' => sprintf('Deleted %d snapshot options for %s', $deleted, $plugin),
568 'deleted' => $deleted,
569 ], 200);
570 }
571
572 /**
573 * Get the appropriate exporter instance for a plugin
574 *
575 * @param string $plugin Plugin slug
576 * @return Abstract_Plugin_Exporter|\WP_Error
577 */
578 private function get_exporter(string $plugin) {
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 }
595 }
596
597 /**
598 * Get argument schema for export endpoint
599 *
600 * @return array
601 */
602 private function get_export_args(): array {
603 return [
604 'plugin' => [
605 'required' => true,
606 'type' => 'string',
607 'enum' => self::ALLOWED_PLUGINS,
608 'sanitize_callback' => 'sanitize_text_field',
609 ],
610 'type' => [
611 'required' => true,
612 'type' => 'string',
613 'enum' => $this->get_allowed_types(),
614 'sanitize_callback' => 'sanitize_text_field',
615 ],
616 'page' => [
617 'required' => true,
618 'type' => 'integer',
619 'minimum' => 1,
620 'sanitize_callback' => 'absint',
621 ],
622 'is_last_type' => [
623 'required' => false,
624 'type' => 'boolean',
625 'default' => false,
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 ],
638 ];
639 }
640
641 /**
642 * Get argument schema for migrate endpoint
643 *
644 * @return array
645 */
646 private function get_migrate_args(): array {
647 return [
648 'plugin' => [
649 'required' => true,
650 'type' => 'string',
651 'enum' => self::ALLOWED_PLUGINS,
652 'sanitize_callback' => 'sanitize_text_field',
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 ],
665 'type' => [
666 'required' => true,
667 'type' => 'string',
668 'enum' => $this->get_allowed_types(),
669 'sanitize_callback' => 'sanitize_text_field',
670 ],
671 'page' => [
672 'required' => true,
673 'type' => 'integer',
674 'minimum' => 1,
675 'sanitize_callback' => 'absint',
676 ],
677 'is_last_type' => [
678 'required' => false,
679 'type' => 'boolean',
680 'default' => false,
681 ],
682 ];
683 }
684 }
685