PluginProbe ʕ •ᴥ•ʔ
SureForms – Drag & Drop Contact Form & Form Builder, Payment Form, Survey, Quiz & Calculator / 2.12.1
SureForms – Drag & Drop Contact Form & Form Builder, Payment Form, Survey, Quiz & Calculator v2.12.1
2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8 0.0.9 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.1.0 1.1.1 1.1.2 1.10.0 1.10.1 1.11.0 1.12.0 1.12.1 1.12.2 1.12.3 1.13.0 1.13.1 1.13.2 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.8.0 1.9.0 1.9.1 2.0.0 2.0.1 2.0.2 2.1.0 2.1.1 2.2.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.0 2.5.2 2.6.0
sureforms / inc / migrator / base-migrator.php
sureforms / inc / migrator Last commit date
importers 4 weeks ago base-migrator.php 1 month ago block-templates.php 1 month ago bootstrap.php 1 month ago
base-migrator.php
670 lines
1 <?php
2 /**
3 * Base Migrator — abstract template-method parent for every per-source importer.
4 *
5 * Holds the import loop, idempotency map, and unsupported-field tracking shared
6 * across every form-plugin importer. Subclasses implement source-specific
7 * detection (`exist`), enumeration (`get_source_forms`), and field translation
8 * (`build_form_content`).
9 *
10 * Architecture mirrors Fluent Forms' `BaseMigrator` (GPL-2.0+), adapted for
11 * SureForms' block-markup output and WP REST API patterns.
12 *
13 * @package sureforms
14 * @since 2.11.0
15 */
16
17 namespace SRFM\Inc\Migrator;
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit;
21 }
22
23 /**
24 * Base_Migrator
25 *
26 * @since 2.11.0
27 */
28 abstract class Base_Migrator {
29 /**
30 * Option key for the source→SureForms import map.
31 */
32 public const IMPORTED_MAP_OPTION = 'srfm_imported_forms_map';
33
34 /**
35 * Default cap on entries imported per form (overridable via filter).
36 */
37 public const DEFAULT_ENTRY_LIMIT = 1000;
38
39 /**
40 * Operators SureForms' conditional-logic editor exposes per block-type
41 * bucket. Mirrors `src/conditional-logic/conditional-logic-options.json`
42 * in SureForms Pro — keep in sync if that schema changes.
43 */
44 protected const CL_BUCKET_OPERATORS = [
45 'default' => [ '==', '!=', 'null', '!null', 'includes', '!includes', 'startWith', 'endWith', 'matchesPattern', 'doesNotMatchPattern' ],
46 'text' => [ '==', '!=', 'null', '!null', 'includes', '!includes', 'startWith', 'endWith', 'matchesPattern', 'doesNotMatchPattern' ],
47 'number' => [ '==', '!=', '>', '>=', '<', '<=', 'between', 'matchesPattern', 'doesNotMatchPattern' ],
48 'list' => [ '==', '!=', 'in', '!in', 'isSelected', '!isSelected', 'matchesPattern', 'doesNotMatchPattern' ],
49 'checkbox' => [ 'isChecked', '!isChecked', 'matchesPattern', 'doesNotMatchPattern' ],
50 'datepicker' => [ 'datePickerIs', 'isBefore', 'isOnOrBefore', 'isAfter', 'isOnOrAfter' ],
51 'timepicker' => [ 'timePickerIs', 'isBefore', 'isOnOrBefore', 'isAfter', 'isOnOrAfter' ],
52 ];
53
54 /**
55 * Source key — one of: cf7, wpforms, gravity, ninja, caldera.
56 *
57 * Subclasses MUST override.
58 *
59 * @var string
60 */
61 protected $key = '';
62
63 /**
64 * Human-readable source name (shown in admin UI).
65 *
66 * Subclasses MUST override.
67 *
68 * @var string
69 */
70 protected $title = '';
71
72 /**
73 * Field labels that did not have a SureForms equivalent during the last import.
74 *
75 * @var array<int,string>
76 */
77 protected $unsupported_fields = [];
78
79 /**
80 * Slugs already used in the current form, for collision avoidance in
81 * `reserve_slug()`. Reset per form by subclasses before calling
82 * `build_form_content()`.
83 *
84 * @var array<string,int>
85 */
86 protected $used_slugs = [];
87
88 /**
89 * Request-scoped cache of the imported-forms map. `null` until first read.
90 *
91 * The map option is `autoload=false`, so reading it once per request (rather
92 * than once per source form) avoids an N+1 DB hit on the listing endpoints.
93 *
94 * @var array<int|string,mixed>|null
95 */
96 private $imported_map_cache = null;
97
98 /**
99 * List forms in the source plugin, formatted for the picker UI.
100 *
101 * @since 2.11.0
102 * @return array<int,array<string,mixed>>
103 */
104 public function list_forms() {
105 if ( ! $this->exist() ) {
106 return [];
107 }
108 $out = [];
109 foreach ( $this->get_source_forms() as $form ) {
110 $source_id = $this->get_source_form_id( $form );
111 $existing_id = $this->find_existing_srfm_id( $source_id );
112 $out[] = [
113 'id' => $source_id,
114 'name' => $this->get_source_form_name( $form ),
115 'imported_srfm_id' => $existing_id,
116 'imported_srfm_edit_url' => $existing_id
117 ? admin_url( 'post.php?post=' . $existing_id . '&action=edit' )
118 : '',
119 ];
120 }
121 return $out;
122 }
123
124 /**
125 * Count the source forms without resolving per-form import mappings.
126 *
127 * The sources listing only needs a count; calling list_forms() there would
128 * run find_existing_srfm_id() for every form purely to discard the result.
129 *
130 * @since 2.11.0
131 * @return int
132 */
133 public function count_source_forms() {
134 if ( ! $this->exist() ) {
135 return 0;
136 }
137 return count( $this->get_source_forms() );
138 }
139
140 /**
141 * Import (or dry-run) the selected source forms into SureForms.
142 *
143 * Re-imports honour an optional per-source behavior map:
144 * - `update` (default) — overwrite the existing SureForms post.
145 * - `skip` — leave the existing post untouched; report under `skipped`.
146 * - `create` — insert a fresh SureForms post even if one already exists.
147 *
148 * @since 2.11.0
149 *
150 * @param array<int,int|string> $selected_ids List of source form ids. Empty = all.
151 * @param bool $dry_run If true, no posts are inserted; a preview is returned.
152 * @param array<int|string,string> $behavior Per-source-id behavior. Keyed by source id (any cast).
153 * @param string $post_status Status for newly inserted forms; one of `draft`/`publish`.
154 * @param bool $skip_existing Force `skip` for any source form already mapped to a
155 * SureForms post — the onboarding "Import all" uses this so it
156 * cannot silently overwrite forms the user already imported +
157 * hand-edited. Per-form $behavior entries still win.
158 * @return array{imported: array<int,array<string,mixed>>, failed: array<int,string>, skipped: array<int,array<string,mixed>>, unsupported_fields: array<int,string>, preview?: array<string,string>}
159 */
160 public function import_forms( array $selected_ids = [], $dry_run = false, array $behavior = [], $post_status = 'publish', $skip_existing = false ) {
161 $post_status = in_array( $post_status, [ 'draft', 'publish' ], true ) ? $post_status : 'publish';
162
163 $this->unsupported_fields = [];
164 $result = [
165 'imported' => [],
166 'failed' => [],
167 'skipped' => [],
168 'unsupported_fields' => [],
169 ];
170
171 if ( ! $this->exist() ) {
172 return $result;
173 }
174
175 $preview = [];
176 $allowed_actions = [ 'update', 'skip', 'create' ];
177
178 foreach ( $this->get_source_forms() as $form ) {
179 $source_id = $this->get_source_form_id( $form );
180 if ( ! empty( $selected_ids ) && ! in_array( (string) $source_id, array_map( 'strval', $selected_ids ), true ) ) {
181 continue;
182 }
183
184 $content = $this->build_form_content( $form );
185 if ( '' === trim( $content ) ) {
186 $result['failed'][] = $this->get_source_form_name( $form );
187 continue;
188 }
189
190 // SureForms CPT post_content holds only field blocks at top level.
191 // The submit button is NOT a content block — SureForms auto-renders
192 // it from the `_srfm_submit_button_text` meta (set in get_form_metas),
193 // so we must not append a button block here or the form shows two.
194 $markup = $content;
195
196 if ( $dry_run ) {
197 $preview[ (string) $source_id ] = $markup;
198 continue;
199 }
200
201 $metas = $this->get_form_metas( $form );
202 $existing_id = $this->find_existing_srfm_id( $source_id );
203
204 // Resolve the per-form action. Order of precedence:
205 // 1. explicit per-id entry from $behavior (user choice)
206 // 2. `$skip_existing` shortcut when there IS an existing import
207 // 3. default `update` (re-import overwrites — matches Settings UI default).
208 $explicit_action = $behavior[ (string) $source_id ] ?? ( $behavior[ (int) $source_id ] ?? null );
209 if ( is_string( $explicit_action ) && in_array( $explicit_action, $allowed_actions, true ) ) {
210 $action = $explicit_action;
211 } elseif ( $skip_existing && $existing_id ) {
212 $action = 'skip';
213 } else {
214 $action = 'update';
215 }
216
217 if ( $existing_id && 'skip' === $action ) {
218 $result['skipped'][] = [
219 'srfm_id' => $existing_id,
220 'source_id' => $source_id,
221 'name' => $this->get_source_form_name( $form ),
222 'edit_url' => admin_url( 'post.php?post=' . $existing_id . '&action=edit' ),
223 ];
224 continue;
225 }
226
227 if ( $existing_id && 'create' !== $action ) {
228 $post_id = $this->update_form_post( $existing_id, $form, $markup, $metas );
229 } else {
230 $post_id = $this->insert_form_post( $form, $markup, $metas, $post_status );
231 }
232
233 if ( $post_id ) {
234 $this->record_import_mapping( $post_id, $source_id );
235 $result['imported'][] = [
236 'srfm_id' => $post_id,
237 'source_id' => $source_id,
238 'name' => $this->get_source_form_name( $form ),
239 'edit_url' => admin_url( 'post.php?post=' . $post_id . '&action=edit' ),
240 ];
241 } else {
242 $result['failed'][] = $this->get_source_form_name( $form );
243 }
244 }
245
246 $result['unsupported_fields'] = array_values( array_unique( array_filter( $this->unsupported_fields ) ) );
247
248 if ( $dry_run ) {
249 $result['preview'] = $preview;
250 }
251
252 return $result;
253 }
254
255 /**
256 * Source key accessor.
257 *
258 * @since 2.11.0
259 * @return string
260 */
261 public function get_key() {
262 return $this->key;
263 }
264
265 /**
266 * Display title accessor.
267 *
268 * @since 2.11.0
269 * @return string
270 */
271 public function get_title() {
272 return $this->title;
273 }
274
275 /**
276 * Whether the source plugin is currently installed/active.
277 *
278 * @since 2.11.0
279 * @return bool
280 */
281 abstract public function exist();
282
283 /**
284 * Insert a new sureforms_form post.
285 *
286 * @since 2.11.0
287 *
288 * @param array<string,mixed> $form Source form descriptor (for title).
289 * @param string $markup Block markup for post_content.
290 * @param array<string,mixed> $metas Optional meta_input payload (key => value).
291 * @param string $post_status Status for the new post; one of `draft`/`publish`.
292 * @return int Inserted post id, or 0 on failure.
293 */
294 protected function insert_form_post( array $form, $markup, array $metas = [], $post_status = 'publish' ) {
295 $post_status = in_array( $post_status, [ 'draft', 'publish' ], true ) ? $post_status : 'publish';
296 $args = [
297 'post_type' => SRFM_FORMS_POST_TYPE,
298 'post_status' => $post_status,
299 'post_title' => $this->get_source_form_name( $form ),
300 // wp_insert_post applies wp_unslash to post_content; pre-slash so the
301 // JSON unicode escapes (e.g. <) survive the round-trip.
302 'post_content' => wp_slash( $markup ),
303 ];
304 if ( ! empty( $metas ) ) {
305 $args['meta_input'] = $metas;
306 }
307 $post_id = wp_insert_post( $args, true );
308 if ( is_wp_error( $post_id ) ) {
309 return 0;
310 }
311 $post_id = (int) $post_id;
312
313 // Blocks carry the form's post id in their `formId` attribute, which
314 // SureForms uses at render to resolve per-block conditional-logic classes
315 // (`conditional-trigger`/`conditional-logic`). The id isn't known until
316 // the post exists, so stamp it now and re-save the content.
317 $stamped = $this->apply_form_id_to_blocks( $markup, $post_id );
318 if ( $stamped !== $markup ) {
319 wp_update_post(
320 [
321 'ID' => $post_id,
322 'post_content' => wp_slash( $stamped ),
323 ]
324 );
325 }
326 return $post_id;
327 }
328
329 /**
330 * Update an existing sureforms_form post with re-imported markup.
331 *
332 * @since 2.11.0
333 *
334 * @param int $post_id Existing post id.
335 * @param array<string,mixed> $form Source form descriptor.
336 * @param string $markup New post_content.
337 * @param array<string,mixed> $metas Optional meta_input payload (key => value).
338 * @return int The post id on success, 0 on failure.
339 */
340 protected function update_form_post( $post_id, array $form, $markup, array $metas = [] ) {
341 $args = [
342 'ID' => $post_id,
343 'post_title' => $this->get_source_form_name( $form ),
344 // wp_update_post applies wp_unslash to post_content; pre-slash so the
345 // JSON unicode escapes (e.g. <) survive the round-trip. The form id is
346 // stamped into each block's `formId` so SureForms can resolve per-block
347 // conditional-logic classes at render.
348 'post_content' => wp_slash( $this->apply_form_id_to_blocks( $markup, (int) $post_id ) ),
349 ];
350 if ( ! empty( $metas ) ) {
351 $args['meta_input'] = $metas;
352 }
353 $updated = wp_update_post( $args, true );
354 return is_wp_error( $updated ) ? 0 : (int) $updated;
355 }
356
357 /**
358 * Stamp the form's post id into every SureForms block's `formId` attribute.
359 *
360 * SureForms resolves per-block conditional-logic classes at render from each
361 * block's `formId` (see `Base::$conditional_class`). Migrated markup is built
362 * before the post exists, so the id is injected once it's known.
363 *
364 * Uses a targeted string insert rather than parse_blocks()/serialize_blocks()
365 * on purpose: re-serialising would drop the JSON_HEX escaping the
366 * Block_Templates emitters apply to neutralise hostile labels. Every srfm
367 * block carries at least a `block_id`, so the opening `{` is always followed
368 * by a quoted key — `formId` is inserted ahead of it without touching the
369 * existing (already-escaped) attribute payload.
370 *
371 * @since 2.11.0
372 *
373 * @param string $markup Serialized block markup.
374 * @param int $form_id Target form post id.
375 * @return string
376 */
377 protected function apply_form_id_to_blocks( $markup, $form_id ) {
378 $form_id = (int) $form_id;
379 if ( $form_id <= 0 || '' === trim( (string) $markup ) ) {
380 return (string) $markup;
381 }
382 return (string) preg_replace(
383 '/(<!--\s+wp:srfm\/[A-Za-z0-9-]+\s+\{)(")/',
384 '${1}"formId":"' . $form_id . '",$2',
385 (string) $markup
386 );
387 }
388
389 /**
390 * Find the existing SureForms post id (if any) that was imported from a
391 * given source identifier.
392 *
393 * @since 2.11.0
394 *
395 * @param int|string $source_id Source form id.
396 * @return int 0 if not previously imported.
397 */
398 protected function find_existing_srfm_id( $source_id ) {
399 $map = $this->get_imported_map();
400 foreach ( $map as $srfm_id => $entry ) {
401 if ( ! is_array( $entry ) ) {
402 continue;
403 }
404 $entry_source_id = $entry['source_id'] ?? '';
405 if ( ! is_scalar( $entry_source_id ) || (string) $entry_source_id !== (string) $source_id ) {
406 continue;
407 }
408 if ( ( $entry['source_key'] ?? '' ) !== $this->key ) {
409 continue;
410 }
411 // Confirm the SureForms post still exists; otherwise prune stale entry.
412 $post = get_post( (int) $srfm_id );
413 if ( $post && SRFM_FORMS_POST_TYPE === $post->post_type ) {
414 return (int) $srfm_id;
415 }
416 unset( $map[ $srfm_id ] );
417 $this->save_imported_map( $map );
418 return 0;
419 }
420 return 0;
421 }
422
423 /**
424 * Read the imported-forms map once per request (memoized).
425 *
426 * @since 2.11.0
427 * @return array<int|string,mixed>
428 */
429 protected function get_imported_map() {
430 if ( null === $this->imported_map_cache ) {
431 $map = get_option( self::IMPORTED_MAP_OPTION, [] );
432 $this->imported_map_cache = is_array( $map ) ? $map : [];
433 }
434 return $this->imported_map_cache;
435 }
436
437 /**
438 * Persist the imported-forms map and refresh the request cache.
439 *
440 * @since 2.11.0
441 *
442 * @param array<int|string,mixed> $map Map to store.
443 * @return void
444 */
445 protected function save_imported_map( array $map ) {
446 $this->imported_map_cache = $map;
447 update_option( self::IMPORTED_MAP_OPTION, $map, false );
448 }
449
450 /**
451 * Record an import mapping for future idempotency checks.
452 *
453 * @since 2.11.0
454 *
455 * @param int $srfm_id Newly inserted/updated SureForms post id.
456 * @param int|string $source_id Source-plugin form id.
457 * @return void
458 */
459 protected function record_import_mapping( $srfm_id, $source_id ) {
460 $map = $this->get_imported_map();
461 $map[ (int) $srfm_id ] = [
462 'source_id' => $source_id,
463 'source_key' => $this->key,
464 'last_imported' => current_time( 'mysql' ),
465 ];
466 $this->save_imported_map( $map );
467 }
468
469 /**
470 * Push a label onto the unsupported-fields list.
471 *
472 * @since 2.11.0
473 *
474 * @param string $label Field label.
475 * @return void
476 */
477 protected function note_unsupported( $label ) {
478 $label = trim( (string) $label );
479 $this->unsupported_fields[] = '' === $label ? __( '(unnamed field)', 'sureforms' ) : $label;
480 }
481
482 /**
483 * Reconcile a converted CL operator against a field's block-type bucket.
484 *
485 * SureForms' CL editor only evaluates a restricted operator set per bucket
486 * (e.g. a `list` field supports `==`/`!=`/`in`, not `includes`/`startWith`).
487 * A source rule whose operator doesn't fit its bucket would import but never
488 * evaluate. This returns a usable bucket — the original when valid, or
489 * `default` when the operator is a text-style comparator the default bucket
490 * accepts — or `''` when no bucket supports the operator (caller drops it).
491 *
492 * @since 2.11.0
493 *
494 * @param string $operator SureForms operator (already mapped from source).
495 * @param string $bucket Field's block-type bucket.
496 * @return string Usable bucket, or '' if the rule should be dropped.
497 */
498 protected function resolve_cl_bucket( $operator, $bucket ) {
499 $ops = self::CL_BUCKET_OPERATORS;
500 if ( isset( $ops[ $bucket ] ) && in_array( $operator, $ops[ $bucket ], true ) ) {
501 return $bucket;
502 }
503 if ( in_array( $operator, $ops['default'], true ) ) {
504 return 'default';
505 }
506 return '';
507 }
508
509 /**
510 * Reserve a unique slug for the current form. Generates a slug from the
511 * seed via `sanitize_title()`. If the slug is already taken in this form,
512 * appends `-2`, `-3`, … until a free slot is found. Tracks the slug in
513 * `$this->used_slugs` so subsequent calls within the same form see the
514 * collision.
515 *
516 * Subclasses must reset `$this->used_slugs = []` at the start of each
517 * `build_form_content()` call.
518 *
519 * @since 2.11.0
520 *
521 * @param string $seed Slug seed (source field name or label).
522 * @return string Deduped slug.
523 */
524 protected function reserve_slug( $seed ) {
525 $base = sanitize_title( (string) $seed );
526 if ( '' === $base ) {
527 $base = 'field';
528 }
529 $slug = $base;
530 $n = 2;
531 while ( isset( $this->used_slugs[ $slug ] ) ) {
532 $slug = $base . '-' . $n;
533 ++$n;
534 }
535 $this->used_slugs[ $slug ] = 1;
536 return $slug;
537 }
538
539 /**
540 * Default confirmation HTML used when the source plugin doesn't ship a
541 * usable thank-you message. Centered heading + body, neutral copy.
542 *
543 * @since 2.11.0
544 *
545 * @return string Confirmation HTML.
546 */
547 protected function default_confirmation_message() {
548 $heading = esc_html__( 'Thank you', 'sureforms' );
549 $body = esc_html__(
550 "Your form has been submitted successfully. We'll review your details and get back to you soon.",
551 'sureforms'
552 );
553 return sprintf(
554 '<h2 style="text-align: center;">%1$s</h2><p style="text-align: center;">%2$s</p>',
555 $heading,
556 $body
557 );
558 }
559
560 /**
561 * Static dispatch from a template-method name to a `Block_Templates`
562 * emitter. Returns an empty string when the method is unknown — the
563 * caller (after applying the `srfm_migrator_block_template` filter) is
564 * responsible for flagging unsupported fields.
565 *
566 * Shared by every importer so add-on subscribers (e.g. SureForms Pro)
567 * can register new emitter names without each importer re-implementing
568 * the dispatch switch.
569 *
570 * @since 2.11.0
571 *
572 * @param string $method Template method name.
573 * @param array<string,mixed> $args Block args.
574 * @return string Block markup, or `''` when unknown.
575 */
576 protected function dispatch_template( $method, array $args ) {
577 switch ( $method ) {
578 case 'input':
579 return Block_Templates::input( $args );
580 case 'email':
581 return Block_Templates::email( $args );
582 case 'url':
583 return Block_Templates::url( $args );
584 case 'phone':
585 return Block_Templates::phone( $args );
586 case 'number':
587 return Block_Templates::number( $args );
588 case 'textarea':
589 return Block_Templates::textarea( $args );
590 case 'dropdown':
591 return Block_Templates::dropdown( $args );
592 case 'multi_choice':
593 return Block_Templates::multi_choice( $args );
594 case 'checkbox':
595 return Block_Templates::checkbox( $args );
596 case 'gdpr':
597 return Block_Templates::gdpr( $args );
598 default:
599 return '';
600 }
601 }
602
603 /**
604 * Return the list of source forms, normalized to ['id', 'name', ...].
605 *
606 * Each element is treated opaquely by the base class and passed back to
607 * the subclass in `get_source_form_id`, `get_source_form_name`,
608 * `build_form_content`.
609 *
610 * @since 2.11.0
611 * @return array<int,array<string,mixed>>
612 */
613 abstract protected function get_source_forms();
614
615 /**
616 * Return the source-side identifier for a given form item.
617 *
618 * @since 2.11.0
619 *
620 * @param array<string,mixed> $form Source form descriptor.
621 * @return int|string
622 */
623 abstract protected function get_source_form_id( array $form );
624
625 /**
626 * Return the source-side display name for a given form item.
627 *
628 * @since 2.11.0
629 *
630 * @param array<string,mixed> $form Source form descriptor.
631 * @return string
632 */
633 abstract protected function get_source_form_name( array $form );
634
635 /**
636 * Build the inner block markup for one source form.
637 *
638 * Implementations should append field labels that fail to map onto
639 * SureForms blocks to `$this->unsupported_fields` so the admin UI can
640 * warn the user.
641 *
642 * @since 2.11.0
643 *
644 * @param array<string,mixed> $form Source form descriptor.
645 * @return string Concatenated field-block markup (without form wrapper).
646 */
647 abstract protected function build_form_content( array $form );
648
649 /**
650 * Build the SureForms post-meta payload for one source form.
651 *
652 * Returns a map of `meta_key => meta_value` that will be passed to
653 * `wp_insert_post()` / `wp_update_post()` via `meta_input`. Keys should
654 * be SureForms' canonical meta keys (e.g. `_srfm_email_notification`,
655 * `_srfm_form_confirmation`). Values must already match the schemas
656 * registered in `inc/post-types.php` — the sanitize_callback there will
657 * still run on import.
658 *
659 * Subclasses without source-side metadata (or where the source format
660 * is incompatible — e.g. CF7) should return a sensible default that
661 * leaves the imported form usable.
662 *
663 * @since 2.11.0
664 *
665 * @param array<string,mixed> $form Source form descriptor.
666 * @return array<string,mixed> Meta key → meta value.
667 */
668 abstract protected function get_form_metas( array $form );
669 }
670