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 |