PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.32.1
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.32.1
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / classes / helpers / FrmXMLHelper.php

FrmXMLHelper.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.32.1, at classes/helpers/FrmXMLHelper.php

2,507 lines 69.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 class FrmXMLHelper {
7
8 /**
9 * @var bool true if importing an XML from API, false if importing an XML file manually.
10 */
11 private static $installing_template = false;
12
13 /**
14 * @param array|string $opt
15 * @param string $padding
16 *
17 * @return void
18 */
19 public static function get_xml_values( $opt, $padding ) {
20 if ( is_array( $opt ) ) {
21 foreach ( $opt as $ok => $ov ) {
22 echo "\n" . esc_html( $padding );
23 $tag = ( is_numeric( $ok ) ? 'key:' : '' ) . $ok;
24 echo '<' . esc_html( $tag ) . '>';
25 self::get_xml_values( $ov, $padding . ' ' );
26
27 if ( is_array( $ov ) ) {
28 echo "\n" . esc_html( $padding );
29 }
30 echo '</' . esc_html( $tag ) . '>';
31 }
32 } else {
33 echo self::cdata( $opt ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
34 }
35 }
36
37 /**
38 * @param string $file
39 *
40 * @return array|WP_Error The items imported
41 */
42 public static function import_xml( $file ) {
43 if ( ! defined( 'WP_IMPORTING' ) ) {
44 define( 'WP_IMPORTING', true );
45 }
46
47 if ( ! class_exists( 'DOMDocument' ) ) {
48 $error_message = sprintf(
49 /* translators: 1: Documentation link */
50 __( 'In order to install XML, your server must have DOMDocument installed. Follow our documentation on %1$s to ensure DOMDocument is properly set up and XML support is enabled.', 'formidable' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
51 '<a href="https://formidableforms.com/knowledgebase/import-forms-entries-and-views/#kb-your-server-does-not-have-xml-enabled" target="_blank">Importing Forms, Entries, and Views</a>' // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
52 );
53
54 return new WP_Error( 'SimpleXML_parse_error', $error_message, libxml_get_errors() );
55 }
56
57 $xml_string = file_get_contents( $file );
58 self::maybe_fix_xml( $xml_string );
59
60 $dom = new DOMDocument();
61
62 // LIBXML_COMPACT activates small nodes allocation optimization.
63 // Use LIBXML_PARSEHUGE to avoid "parser error : internal error: Huge input lookup" for large (300MB) files.
64 $success = $dom->loadXML( $xml_string, LIBXML_COMPACT | LIBXML_PARSEHUGE );
65
66 if ( ! $success ) {
67 return new WP_Error( 'SimpleXML_parse_error', __( 'There was an error when reading this XML file', 'formidable' ), libxml_get_errors() );
68 }
69
70 if ( ! function_exists( 'simplexml_import_dom' ) ) {
71 return new WP_Error( 'SimpleXML_parse_error', __( 'Your server is missing the simplexml_import_dom function', 'formidable' ), libxml_get_errors() );
72 }
73
74 $xml = simplexml_import_dom( $dom );
75 unset( $dom );
76
77 // Halt if loading produces an error
78 if ( ! $xml ) {
79 return new WP_Error( 'SimpleXML_parse_error', __( 'There was an error when reading this XML file', 'formidable' ), libxml_get_errors() );
80 }
81
82 return self::import_xml_now( $xml );
83 }
84
85 /**
86 * @since 6.2.3
87 *
88 * @param string $xml_string
89 *
90 * @return void
91 */
92 private static function maybe_fix_xml( &$xml_string ) {
93 if ( ! str_starts_with( $xml_string, '<?xml' ) ) {
94 // Some XML files have may have unexpected characters at the start.
95 $xml_string = substr( $xml_string, strpos( $xml_string, '<?xml' ) );
96 }
97
98 // The Equity theme adds a <meta name="generator" content="Equity 1.7.13" /> tag using the "the_generator" filter.
99 // Strip that out as it breaks the XML import.
100 $channel_start_position = strpos( $xml_string, '<channel>' );
101 $content_before_channel_tag = substr( $xml_string, 0, $channel_start_position );
102
103 if ( str_starts_with( $content_before_channel_tag, '<meta name="generator" ' ) ) {
104 return;
105 }
106
107 $content_before_channel_tag = preg_replace(
108 '/<meta\s+[^>]*name="generator"[^>]*\/>/i',
109 '',
110 $content_before_channel_tag,
111 1
112 );
113 $xml_string = $content_before_channel_tag . substr( $xml_string, $channel_start_position );
114 }
115
116 /**
117 * Add terms, forms (form and field ids), posts (post ids), and entries to db, in that order
118 *
119 * @since 3.06
120 *
121 * @param object $xml
122 * @param bool $installing_template
123 *
124 * @return array The number of items imported
125 */
126 public static function import_xml_now( $xml, $installing_template = false ) {
127 if ( ! defined( 'WP_IMPORTING' ) ) {
128 define( 'WP_IMPORTING', true );
129 }
130
131 self::$installing_template = $installing_template;
132 $imported = self::pre_import_data();
133
134 foreach ( array( 'term', 'form', 'view' ) as $item_type ) {
135 // Grab cats, tags, and terms, or forms or posts.
136 if ( ! isset( $xml->{$item_type} ) ) {
137 continue;
138 }
139
140 $function_name = 'import_xml_' . $item_type . 's';
141 $imported = self::$function_name( $xml->{$item_type}, $imported );
142 unset( $function_name, $xml->{$item_type} );
143 }
144
145 $imported = apply_filters( 'frm_importing_xml', $imported, $xml );
146
147 if ( empty( $imported['form_status'] ) ) {
148 // Check for an error message in the XML.
149 if ( isset( $xml->Code ) && isset( $xml->Message ) ) { // phpcs:ignore WordPress.NamingConventions
150 $imported['error'] = (string) $xml->Message; // phpcs:ignore WordPress.NamingConventions
151 }
152 }
153
154 return $imported;
155 }
156
157 /**
158 * @since 3.06
159 *
160 * @return array
161 */
162 private static function pre_import_data() {
163 $defaults = array(
164 'forms' => 0,
165 'fields' => 0,
166 'terms' => 0,
167 'posts' => 0,
168 'views' => 0,
169 'actions' => 0,
170 'styles' => 0,
171 );
172
173 return array(
174 'imported' => $defaults,
175 'updated' => $defaults,
176 'forms' => array(),
177 'terms' => array(),
178 );
179 }
180
181 /**
182 * @param SimpleXMLElement $terms
183 * @param array $imported
184 *
185 * @return array
186 */
187 public static function import_xml_terms( $terms, $imported ) {
188 foreach ( $terms as $t ) {
189 if ( term_exists( (string) $t->term_slug, (string) $t->term_taxonomy ) ) {
190 continue;
191 }
192
193 $parent = self::get_term_parent_id( $t );
194
195 $term = wp_insert_term(
196 (string) $t->term_name,
197 (string) $t->term_taxonomy,
198 array(
199 'slug' => (string) $t->term_slug,
200 'description' => (string) $t->term_description,
201 'parent' => $parent ? $parent : 0,
202 )
203 );
204
205 if ( $term && is_array( $term ) ) {
206 ++$imported['imported']['terms'];
207 $imported['terms'][ (int) $t->term_id ] = $term['term_id'];
208 }
209
210 unset( $term, $t );
211 }//end foreach
212
213 return $imported;
214 }
215
216 /**
217 * @since 2.0.8
218 *
219 * @param object $t
220 *
221 * @return int|string
222 */
223 private static function get_term_parent_id( $t ) {
224 $parent = (string) $t->term_parent;
225
226 if ( $parent ) {
227 $parent = term_exists( (string) $t->term_parent, (string) $t->term_taxonomy );
228 $parent = $parent ? $parent['term_id'] : 0;
229 }
230
231 return $parent;
232 }
233
234 /**
235 * @param SimpleXMLElement $forms
236 * @param array $imported
237 *
238 * @return array
239 */
240 public static function import_xml_forms( $forms, $imported ) {
241 $child_forms = array();
242
243 // Import child forms first
244 self::put_child_forms_first( $forms );
245
246 foreach ( $forms as $item ) {
247 $form = self::fill_form( $item );
248
249 self::update_custom_style_setting_on_import( $form );
250
251 $this_form = self::maybe_get_form( $form );
252 $old_id = false;
253 $form_fields = false;
254
255 if ( $this_form ) {
256 $form_id = $this_form->id;
257 $old_id = $this_form->id;
258 self::update_form( $this_form, $form, $imported );
259
260 $form_fields = self::get_form_fields( $form_id );
261 } else {
262 $form_id = FrmForm::create( $form );
263
264 if ( $form_id ) {
265 if ( empty( $form['parent_form_id'] ) ) {
266 // Don't include the repeater form in the imported count.
267 ++$imported['imported']['forms'];
268 }
269
270 // Keep track of whether this specific form was updated or not.
271 $imported['form_status'][ $form_id ] = 'imported';
272 }
273 }
274
275 if ( $form_id ) {
276 self::track_imported_child_forms( (int) $form_id, $form['parent_form_id'], $child_forms );
277 }
278
279 self::import_xml_fields( $item->field, $form_id, $this_form, $form_fields, $imported );
280
281 self::delete_removed_fields( $form_fields );
282
283 // Update field ids/keys to new ones.
284 do_action( 'frm_after_duplicate_form', $form_id, $form, array( 'old_id' => $old_id ) );
285
286 $imported['forms'][ (int) $item->id ] = $form_id;
287
288 // Send pre 2.0 form options through function that creates actions.
289 self::migrate_form_settings_to_actions( $form['options'], $form_id, $imported, true );
290
291 do_action( 'frm_after_import_form', $form_id, $form );
292
293 unset( $form, $item );
294 }//end foreach
295
296 self::maybe_update_child_form_parent_id( $imported['forms'], $child_forms );
297
298 /**
299 * Fires after all forms in the XML have been imported.
300 *
301 * Child forms are imported before their parent form, so per-form fix-ups
302 * cannot resolve conditional logic that references fields in a form
303 * imported later. Use this action for cross-form fix-ups that require
304 * the fully populated `$frm_duplicate_ids` map.
305 *
306 * @since 6.31
307 *
308 * @param array $imported Summary of imported items, including
309 * 'forms' => array( old_form_id => new_form_id ).
310 */
311 do_action( 'frm_after_import_forms', $imported );
312
313 return $imported;
314 }
315
316 /**
317 * @param object $item
318 *
319 * @return array
320 */
321 private static function fill_form( $item ) {
322 $form = array(
323 'id' => (int) $item->id,
324 'form_key' => (string) $item->form_key,
325 'name' => (string) $item->name,
326 'description' => (string) $item->description,
327 'options' => (string) $item->options,
328 'logged_in' => (int) $item->logged_in,
329 'is_template' => (int) $item->is_template,
330 'editable' => (int) $item->editable,
331 'status' => (string) $item->status,
332 'parent_form_id' => isset( $item->parent_form_id ) ? (int) $item->parent_form_id : 0,
333 'created_at' => gmdate( 'Y-m-d H:i:s', strtotime( (string) $item->created_at ) ),
334 );
335
336 if ( empty( $item->created_at ) ) {
337 $form['created_at'] = current_time( 'mysql', 1 );
338 }
339
340 $form['options'] = FrmAppHelper::maybe_json_decode( $form['options'] );
341
342 if ( self::$installing_template ) {
343 // Templates don't necessarily have antispam on, but we want our templates to all have antispam on by default.
344 $form['options']['antispam'] = 1;
345 }
346
347 return $form;
348 }
349
350 /**
351 * @param array $form
352 *
353 * @return false|object
354 */
355 private static function maybe_get_form( $form ) {
356 // If template, allow to edit if form keys match, otherwise, creation date must also match
357 $edit_query = array(
358 'form_key' => $form['form_key'],
359 'is_template' => $form['is_template'],
360 );
361
362 if ( ! $form['is_template'] ) {
363 $edit_query['created_at'] = $form['created_at'];
364 }
365
366 $edit_query = apply_filters( 'frm_match_xml_form', $edit_query, $form );
367 $form = FrmForm::getAll( $edit_query, '', 1 );
368
369 if ( is_object( $form ) && $form->status === 'trash' ) {
370 FrmForm::destroy( $form->id );
371 return false;
372 }
373
374 return $form;
375 }
376
377 /**
378 * @param object $this_form
379 * @param array $form
380 * @param array $imported
381 *
382 * @return void
383 */
384 private static function update_form( $this_form, $form, &$imported ) {
385 $form_id = $this_form->id;
386 FrmForm::update( $form_id, $form );
387
388 if ( empty( $form['parent_form_id'] ) ) {
389 // Don't include the repeater form in the updated count.
390 ++$imported['updated']['forms'];
391 }
392
393 // Keep track of whether this specific form was updated or not
394 $imported['form_status'][ $form_id ] = 'updated';
395 }
396
397 /**
398 * @param int|string $form_id
399 *
400 * @return array
401 */
402 private static function get_form_fields( $form_id ) {
403 $form_fields = FrmField::get_all_for_form( $form_id, '', 'exclude', 'exclude' );
404 $old_fields = array();
405
406 foreach ( $form_fields as $f ) {
407 $old_fields[ $f->id ] = $f;
408 $old_fields[ $f->field_key ] = $f->id;
409 unset( $f );
410 }
411
412 return $old_fields;
413 }
414
415 /**
416 * Delete any fields attached to this form that were not included in the template
417 *
418 * @param array $form_fields
419 *
420 * @return void
421 */
422 private static function delete_removed_fields( $form_fields ) {
423 if ( ! $form_fields ) {
424 return;
425 }
426
427 foreach ( $form_fields as $field ) {
428 if ( is_object( $field ) ) {
429 FrmField::destroy( $field->id );
430 }
431 unset( $field );
432 }
433 }
434
435 /**
436 * Put child forms first so they will be imported before parents
437 *
438 * @since 2.0.16
439 *
440 * @param array $forms
441 */
442 private static function put_child_forms_first( &$forms ) {
443 $child_forms = array();
444 $regular_forms = array();
445
446 foreach ( $forms as $form ) {
447 $parent_form_id = isset( $form->parent_form_id ) ? (int) $form->parent_form_id : 0;
448
449 if ( $parent_form_id ) {
450 $child_forms[] = $form;
451 } else {
452 $regular_forms[] = $form;
453 }
454 }
455
456 $forms = array_merge( $child_forms, $regular_forms );
457 }
458
459 /**
460 * Keep track of all imported child forms
461 *
462 * @since 2.0.16
463 *
464 * @param int $form_id
465 * @param int $parent_form_id
466 * @param array $child_forms
467 */
468 private static function track_imported_child_forms( $form_id, $parent_form_id, &$child_forms ) {
469 if ( $parent_form_id ) {
470 $child_forms[ $form_id ] = $parent_form_id;
471 }
472 }
473
474 /**
475 * Update the parent_form_id on imported child forms
476 * Child forms are imported first so their parent_form_id will need to be updated after the parent is imported
477 *
478 * @since 2.0.6
479 *
480 * @param array $imported_forms
481 * @param array $child_forms
482 */
483 private static function maybe_update_child_form_parent_id( $imported_forms, $child_forms ) {
484 foreach ( $child_forms as $child_form_id => $old_parent_form_id ) {
485 if ( ! isset( $imported_forms[ $old_parent_form_id ] ) || (int) $imported_forms[ $old_parent_form_id ] === (int) $old_parent_form_id ) {
486 continue;
487 }
488
489 // Update all children with this old parent_form_id
490 $new_parent_form_id = (int) $imported_forms[ $old_parent_form_id ];
491 FrmForm::update( $child_form_id, array( 'parent_form_id' => $new_parent_form_id ) );
492 do_action( 'frm_update_child_form_parent_id', $child_form_id, $new_parent_form_id );
493 }
494 }
495
496 /**
497 * Import all fields for a form
498 *
499 * @since 2.0.13
500 *
501 * TODO: Cut down on params
502 *
503 * @param SimpleXMLElement $xml_fields
504 * @param int|string $form_id
505 * @param object $this_form
506 * @param array $form_fields
507 * @param array $imported
508 */
509 private static function import_xml_fields( $xml_fields, $form_id, $this_form, &$form_fields, &$imported ) {
510 $in_section = 0;
511 $keys_by_original_field_id = array();
512
513 foreach ( $xml_fields as $field ) {
514 $f = self::fill_field( $field, $form_id );
515
516 self::set_default_value( $f );
517 self::maybe_add_required( $f );
518 self::maybe_update_in_section_variable( $in_section, $f );
519 self::maybe_update_form_select( $f, $imported );
520 self::maybe_update_get_values_form_setting( $imported, $f );
521 self::migrate_placeholders( $f );
522
523 if ( $this_form ) {
524 // Check for field to edit by field id
525 if ( isset( $form_fields[ $f['id'] ] ) ) {
526 FrmField::update( $f['id'], $f );
527 ++$imported['updated']['fields'];
528
529 unset( $form_fields[ $f['id'] ] );
530
531 // Unset old field key.
532 if ( isset( $form_fields[ $f['field_key'] ] ) ) {
533 unset( $form_fields[ $f['field_key'] ] );
534 }
535 } elseif ( isset( $form_fields[ $f['field_key'] ] ) ) {
536 $keys_by_original_field_id[ $f['id'] ] = $f['field_key'];
537 $old_field_id = $f['id'];
538
539 // Check for field to edit by field key
540 unset( $f['id'] );
541
542 FrmField::update( $form_fields[ $f['field_key'] ], $f );
543 ++$imported['updated']['fields'];
544
545 self::do_after_field_imported_action( $f, $form_fields, $old_field_id );
546
547 // Unset old field id.
548 unset( $form_fields[ $form_fields[ $f['field_key'] ] ] );
549
550 // Unset old field key.
551 unset( $form_fields[ $f['field_key'] ] );
552 } else {
553 // If no matching field id or key in this form, create the field.
554 self::create_imported_field( $f, $imported );
555 }//end if
556 } else {
557 self::create_imported_field( $f, $imported );
558 }//end if
559 }//end foreach
560
561 if ( $keys_by_original_field_id ) {
562 self::maybe_update_field_ids( $form_id, $keys_by_original_field_id );
563 }
564 }
565
566 /**
567 * @since 6.8.4
568 *
569 * @param array $field_array
570 *
571 * @return array
572 */
573 private static function update_field_options_with_defaults( $field_array ) {
574 $defaults = self::default_field_options( $field_array['type'] );
575 $field_array['field_options'] = array_merge( $defaults, $field_array['field_options'] );
576
577 return $field_array;
578 }
579
580 /**
581 * @since 6.8.4
582 *
583 * @param array $field_array
584 * @param array $form_fields
585 * @param int $old_field_id
586 *
587 * @return void
588 */
589 private static function do_after_field_imported_action( $field_array, $form_fields, $old_field_id ) {
590 // Assign field array the update field's ID.
591 $field_array['id'] = $form_fields[ $field_array['field_key'] ];
592
593 /**
594 * Fires when an existing field is imported.
595 *
596 * @since 6.8.4
597 *
598 * @param array $field_array
599 * @param int $field_id
600 * @param int $old_field_id
601 */
602 do_action( 'frm_after_existing_field_is_imported', $field_array, $form_fields[ $field_array['field_key'] ], $old_field_id );
603 }
604
605 /**
606 * @param object $field
607 * @param int|string $form_id
608 *
609 * @return array
610 */
611 private static function fill_field( $field, $form_id ) {
612 return array(
613 'id' => (int) $field->id,
614 'field_key' => (string) $field->field_key,
615 'name' => (string) $field->name,
616 'description' => (string) $field->description,
617 'type' => (string) $field->type,
618 'default_value' => FrmAppHelper::maybe_json_decode( (string) $field->default_value ),
619 'field_order' => (int) $field->field_order,
620 'form_id' => (int) $form_id,
621 'required' => (int) $field->required,
622 'options' => FrmAppHelper::maybe_json_decode( (string) $field->options ),
623 'field_options' => FrmAppHelper::maybe_json_decode( (string) $field->field_options ),
624 );
625 }
626
627 /**
628 * @since 4.06
629 *
630 * @param array $f
631 *
632 * @return void
633 */
634 private static function set_default_value( &$f ) {
635 $has_default = array(
636 'text',
637 'email',
638 'url',
639 'textarea',
640 'number',
641 'phone',
642 'date',
643 'hidden',
644 'password',
645 'tag',
646 );
647
648 if ( ! is_array( $f['default_value'] ) || ! in_array( $f['type'], $has_default, true ) ) {
649 return;
650 }
651
652 if ( count( $f['default_value'] ) === 1 ) {
653 $f['default_value'] = '[' . reset( $f['default_value'] ) . ']';
654 } else {
655 $f['default_value'] = reset( $f['default_value'] );
656 }
657 }
658
659 /**
660 * Make sure the required indicator is set.
661 *
662 * @since 4.05
663 *
664 * @param array $f
665 *
666 * @return void
667 */
668 private static function maybe_add_required( &$f ) {
669 if ( $f['required'] && ! isset( $f['field_options']['required_indicator'] ) ) {
670 $f['field_options']['required_indicator'] = '*';
671 }
672 }
673
674 /**
675 * Update the current in_section value at the beginning of the field loop
676 *
677 * @since 2.0.25
678 *
679 * @param int $in_section
680 * @param array $f
681 *
682 * @return void
683 */
684 private static function maybe_update_in_section_variable( &$in_section, &$f ) {
685 // If we're at the end of a section, switch $in_section is 0
686 if ( in_array( $f['type'], array( 'end_divider', 'break', 'form' ), true ) ) {
687 $in_section = 0;
688 }
689
690 // Update the current field's in_section value
691 if ( ! isset( $f['field_options']['in_section'] ) ) {
692 $f['field_options']['in_section'] = $in_section;
693 }
694
695 // If we're starting a new section, switch $in_section to ID of divider
696 if ( $f['type'] === 'divider' ) {
697 $in_section = $f['id'];
698 }
699 }
700
701 /**
702 * Switch the form_select on a repeating field or embedded form if it needs to be switched
703 *
704 * @since 2.0.16
705 *
706 * @param array $f
707 * @param array $imported
708 */
709 private static function maybe_update_form_select( &$f, $imported ) {
710 if ( ! isset( $imported['forms'] ) ) {
711 return;
712 }
713
714 if ( $f['type'] !== 'form' && ( $f['type'] !== 'divider' || ! FrmField::is_option_true( $f['field_options'], 'repeat' ) ) ) {
715 return;
716 }
717
718 if ( ! FrmField::is_option_true( $f['field_options'], 'form_select' ) ) {
719 return;
720 }
721
722 $form_select = (int) $f['field_options']['form_select'];
723
724 if ( isset( $imported['forms'][ $form_select ] ) ) {
725 $f['field_options']['form_select'] = $imported['forms'][ $form_select ];
726 }
727 }
728
729 /**
730 * Update the get_values_form setting if the form was imported
731 *
732 * @since 2.01.0
733 *
734 * @param array $imported
735 * @param array $f
736 *
737 * @return void
738 */
739 private static function maybe_update_get_values_form_setting( $imported, &$f ) {
740 if ( ! isset( $imported['forms'] ) ) {
741 return;
742 }
743
744 if ( ! FrmField::is_option_true_in_array( $f['field_options'], 'get_values_form' ) ) {
745 return;
746 }
747
748 $old_form = $f['field_options']['get_values_form'];
749
750 if ( isset( $imported['forms'][ $old_form ] ) ) {
751 $f['field_options']['get_values_form'] = $imported['forms'][ $old_form ];
752 }
753 }
754
755 /**
756 * @since 4.0
757 *
758 * @param array $f
759 *
760 * @return void
761 */
762 private static function migrate_placeholders( &$f ) {
763 $update_values = self::migrate_field_placeholder( $f, 'clear_on_focus' );
764
765 foreach ( $update_values as $k => $v ) {
766 $f[ $k ] = $v;
767 }
768
769 $update_values = self::migrate_field_placeholder( $f, 'default_blank' );
770
771 foreach ( $update_values as $k => $v ) {
772 $f[ $k ] = $v;
773 }
774 }
775
776 /**
777 * Move clear_on_focus or default_blank to placeholder.
778 * Also called during database migration in FrmMigrate.
779 *
780 * @since 4.0
781 *
782 * @param array|object $field
783 * @param string $type
784 *
785 * @return array
786 */
787 public static function migrate_field_placeholder( $field, $type ) {
788 $field = (array) $field;
789 $field_options = $field['field_options'];
790
791 if ( empty( $field_options[ $type ] ) || empty( $field['default_value'] ) ) {
792 return array();
793 }
794
795 $field_options['placeholder'] = is_array( $field['default_value'] ) ? reset( $field['default_value'] ) : $field['default_value'];
796 unset( $field_options['default_blank'], $field_options['clear_on_focus'] );
797
798 $changes = array(
799 'field_options' => $field_options,
800 'default_value' => '',
801 );
802
803 // If a dropdown placeholder was used, remove the option so it won't be included twice.
804 $options = $field['options'];
805
806 if ( $type !== 'default_blank' || ! is_array( $options ) ) {
807 return $changes;
808 }
809
810 $default_value = $field['default_value'];
811
812 if ( is_array( $default_value ) ) {
813 $default_value = reset( $default_value );
814 }
815
816 foreach ( $options as $opt_key => $opt ) {
817 if ( is_array( $opt ) ) {
818 $opt = $opt['value'] ?? $opt['label'] ?? reset( $opt );
819 }
820
821 // phpcs:ignore Universal.Operators.StrictComparisons
822 if ( $opt == $default_value ) {
823 unset( $options[ $opt_key ] );
824 break;
825 }
826 }
827
828 $changes['options'] = $options;
829 // end if
830
831 return $changes;
832 }
833
834 /**
835 * Create an imported field
836 *
837 * @since 2.0.25
838 *
839 * @param array $f
840 * @param array $imported
841 *
842 * @return void
843 */
844 private static function create_imported_field( $f, &$imported ) {
845 $f = self::update_field_options_with_defaults( $f );
846
847 if ( is_callable( 'FrmProFileImport::import_attachment' ) ) {
848 $f = self::maybe_import_images_for_options( $f );
849 }
850
851 $new_id = FrmField::create( $f );
852
853 if ( ! $new_id ) {
854 return;
855 }
856
857 ++$imported['imported']['fields'];
858 do_action( 'frm_after_field_is_imported', $f, $new_id );
859 }
860
861 /**
862 * Import images for radio buttons and checkboxes from image src if available.
863 *
864 * @since 5.5.1
865 *
866 * @param array $field
867 *
868 * @return array
869 */
870 private static function maybe_import_images_for_options( $field ) {
871 if ( empty( $field['options'] ) || ! is_array( $field['options'] ) ) {
872 return $field;
873 }
874
875 foreach ( $field['options'] as $key => $option ) {
876 if ( ! is_array( $option ) || empty( $option['src'] ) ) {
877 continue;
878 }
879
880 $field_object = (object) $field;
881 // Fake the file type as FrmProImport::import_attachment checks for file type.
882 $field_object->type = 'file';
883
884 $image_id = FrmProFileImport::import_attachment( $option['src'], $field_object );
885 // Remove the src from options as it isn't required after import.
886 unset( $field['options'][ $key ]['src'] );
887
888 if ( is_numeric( $image_id ) ) {
889 $field['options'][ $key ]['image'] = $image_id;
890 }
891 }
892
893 return $field;
894 }
895
896 /**
897 * Fix field ids for fields that already exist prior to import.
898 *
899 * @since 4.07
900 *
901 * @param int $form_id
902 * @param array $keys_by_original_field_id
903 *
904 * @return void
905 */
906 protected static function maybe_update_field_ids( $form_id, $keys_by_original_field_id ) {
907 global $frm_duplicate_ids;
908
909 $former_duplicate_ids = $frm_duplicate_ids;
910 $where = array(
911 array(
912 'or' => 1,
913 'fi.form_id' => $form_id,
914 'fr.parent_form_id' => $form_id,
915 ),
916 );
917 $fields = FrmField::getAll( $where, 'field_order' );
918 $field_id_by_key = wp_list_pluck( $fields, 'id', 'field_key' );
919
920 foreach ( $fields as $field ) {
921 $before = (array) clone $field;
922 $field = (array) $field;
923 $frm_duplicate_ids = $keys_by_original_field_id;
924 $after = FrmFieldsHelper::switch_field_ids( $field );
925
926 if ( $before['field_options'] === $after['field_options'] ) {
927 continue;
928 }
929
930 $frm_duplicate_ids = $field_id_by_key;
931 $after = FrmFieldsHelper::switch_field_ids( $after );
932
933 if ( $before['field_options'] !== $after['field_options'] ) {
934 FrmField::update( $field['id'], array( 'field_options' => $after['field_options'] ) );
935 }
936 }
937
938 $frm_duplicate_ids = $former_duplicate_ids;
939 }
940
941 /**
942 * Updates the custom style setting on import
943 * Convert the post slug to an ID
944 *
945 * @since 2.0.19
946 *
947 * @param array $form
948 *
949 * @return void
950 */
951 private static function update_custom_style_setting_on_import( &$form ) {
952 if ( ! isset( $form['options']['custom_style'] ) ) {
953 return;
954 }
955
956 if ( is_numeric( $form['options']['custom_style'] ) && 1 === intval( $form['options']['custom_style'] ) ) {
957 // Set to default
958 $form['options']['custom_style'] = 1;
959
960 return;
961 }
962
963 // Replace the style name with the style ID on import
964 global $wpdb;
965 $table = $wpdb->prefix . 'posts';
966 $where = array(
967 'post_name' => $form['options']['custom_style'],
968 'post_type' => 'frm_styles',
969 );
970 $select = 'ID';
971 $style_id = FrmDb::get_var( $table, $where, $select );
972
973 if ( $style_id ) {
974 $form['options']['custom_style'] = $style_id;
975 return;
976 }
977
978 // Save the old style to maybe update after styles import
979 $form['options']['old_style'] = $form['options']['custom_style'];
980
981 // Set to default
982 $form['options']['custom_style'] = 1;
983 }
984
985 /**
986 * After styles are imported, check for any forms that were linked
987 * and link them back up.
988 *
989 * @since 2.2.7
990 *
991 * @param int|string $form_id
992 *
993 * @return void
994 */
995 private static function update_custom_style_setting_after_import( $form_id ) {
996 $form = FrmForm::getOne( $form_id );
997
998 if ( ! $form || ! isset( $form->options['old_style'] ) ) {
999 return;
1000 }
1001
1002 $form = (array) $form;
1003 $saved_style = $form['options']['custom_style'];
1004 $form['options']['custom_style'] = $form['options']['old_style'];
1005 self::update_custom_style_setting_on_import( $form );
1006 $has_changed = $form['options']['custom_style'] != $saved_style && $form['options']['custom_style'] != $form['options']['old_style']; // phpcs:ignore Universal.Operators.StrictComparisons, SlevomatCodingStandard.Files.LineLength.LineTooLong
1007
1008 if ( $has_changed ) {
1009 FrmForm::update( $form['id'], $form );
1010 }
1011 }
1012
1013 /**
1014 * Handle import for all posts. This includes views, styles, pages, and form actions.
1015 *
1016 * @param SimpleXMLElement $views
1017 * @param array $imported
1018 *
1019 * @return array
1020 */
1021 public static function import_xml_views( $views, $imported ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
1022 $imported['posts'] = array();
1023 $form_action_type = FrmFormActionsController::$action_post_type;
1024
1025 $post_types = array(
1026 'frm_display' => 'views',
1027 $form_action_type => 'actions',
1028 'frm_styles' => 'styles',
1029 );
1030
1031 $view_ids = array();
1032 $posts_with_shortcodes = array();
1033
1034 foreach ( $views as $item ) {
1035 $post = array(
1036 'post_title' => (string) $item->title,
1037 'post_name' => (string) $item->post_name,
1038 'post_type' => (string) $item->post_type,
1039 'post_password' => (string) $item->post_password,
1040 'guid' => (string) $item->guid,
1041 'post_status' => (string) $item->status,
1042 'post_author' => FrmAppHelper::get_user_id_param( (string) $item->post_author ),
1043 'post_id' => (int) $item->post_id,
1044 'post_parent' => (int) $item->post_parent,
1045 'menu_order' => (int) $item->menu_order,
1046 'post_content' => FrmFieldsHelper::switch_field_ids( (string) $item->content ),
1047 'post_excerpt' => FrmFieldsHelper::switch_field_ids( (string) $item->excerpt ),
1048 'is_sticky' => (string) $item->is_sticky,
1049 'comment_status' => (string) $item->comment_status,
1050 'post_date' => (string) $item->post_date,
1051 'post_date_gmt' => (string) $item->post_date_gmt,
1052 'ping_status' => (string) $item->ping_status,
1053 'postmeta' => array(),
1054 'layout' => array(),
1055 'tax_input' => array(),
1056 );
1057
1058 $post['post_content'] = self::switch_form_ids( $post['post_content'], $imported['forms'] );
1059
1060 // Fix issue with line breaks appearing in descriptions as "rn".
1061 if ( $post['post_type'] === $form_action_type ) {
1062 $post['post_content'] = str_replace( '\\\\r\\\\n', '\\r\\n', $post['post_content'] );
1063 }
1064
1065 $old_id = $post['post_id'];
1066 self::populate_post( $post, $item, $imported );
1067
1068 unset( $item );
1069
1070 $post_id = false;
1071
1072 if ( $post['post_type'] === $form_action_type ) {
1073 $action_control = FrmFormActionsController::get_form_actions( $post['post_excerpt'] );
1074
1075 if ( $action_control && is_object( $action_control ) && isset( $imported['form_status'] ) ) {
1076 $post_id = $action_control->maybe_create_action( $post, $imported['form_status'] );
1077 }
1078 unset( $action_control );
1079 } elseif ( $post['post_type'] === 'frm_styles' ) {
1080 // Properly encode post content before inserting the post
1081 $post['post_content'] = FrmAppHelper::maybe_json_decode( $post['post_content'] );
1082 $post['post_content'] = FrmAppHelper::prepare_and_encode( $post['post_content'] );
1083 // Store template data additionally to a postmeta that can be used to revert the style to default template style.
1084 $post['postmeta']['frm_style_default'] = $post['post_content'];
1085
1086 // Create/update post now
1087 $post_id = wp_insert_post( $post );
1088 } else {
1089 if ( $post['post_type'] === 'frm_display' ) {
1090 $post['post_content'] = self::maybe_prepare_json_view_content( $post['post_content'] );
1091 } elseif ( 'page' === $post['post_type'] && isset( $imported['posts'][ $post['post_parent'] ] ) ) {
1092 $post['post_parent'] = $imported['posts'][ $post['post_parent'] ];
1093 }
1094 // Create/update post now
1095 $post_id = wp_insert_post( $post );
1096 }//end if
1097
1098 if ( ! is_numeric( $post_id ) ) {
1099 continue;
1100 }
1101
1102 if ( str_contains( $post['post_content'], '[display-frm-data' ) || str_contains( $post['post_content'], '[formidable' ) ) {
1103 $posts_with_shortcodes[ $post_id ] = $post;
1104 }
1105
1106 self::update_postmeta( $post, $post_id );
1107 self::update_layout( $post, $post_id );
1108
1109 $this_type = 'posts';
1110
1111 if ( isset( $post_types[ $post['post_type'] ] ) ) {
1112 $this_type = $post_types[ $post['post_type'] ];
1113 }
1114
1115 if ( isset( $post['ID'] ) && (int) $post_id === (int) $post['ID'] ) {
1116 ++$imported['updated'][ $this_type ];
1117 } else {
1118 ++$imported['imported'][ $this_type ];
1119 }
1120
1121 $imported['posts'][ $old_id ] = $post_id;
1122
1123 if ( $post['post_type'] === 'frm_display' ) {
1124 $view_ids[ $old_id ] = $post_id;
1125 }
1126
1127 do_action( 'frm_after_import_view', $post_id, $post );
1128
1129 unset( $post );
1130 }//end foreach
1131
1132 if ( $posts_with_shortcodes && $view_ids ) {
1133 self::maybe_switch_view_ids_after_importing_posts( $posts_with_shortcodes, $view_ids );
1134 }
1135 unset( $posts_with_shortcodes, $view_ids );
1136
1137 if ( ! empty( $imported['forms'] ) ) {
1138 // Clear imported forms style cache to make sure the new styles are applied to the forms
1139 self::clear_forms_style_caches( $imported['forms'] );
1140 }
1141
1142 self::maybe_update_stylesheet( $imported );
1143
1144 flush_rewrite_rules();
1145
1146 return $imported;
1147 }
1148
1149 /**
1150 * Clears styles from cache for imported forms
1151 *
1152 * @param array $imported_forms
1153 *
1154 * @return void
1155 */
1156 private static function clear_forms_style_caches( $imported_forms ) {
1157 $where = array(
1158 'id' => $imported_forms,
1159 'options LIKE' => '"old_style"',
1160 );
1161 $forms = FrmDb::get_results( 'frm_forms', $where );
1162
1163 foreach ( $forms as $form ) {
1164 FrmAppHelper::unserialize_or_decode( $form->options );
1165
1166 if ( ! $form->options ) {
1167 continue;
1168 }
1169
1170 $where = array(
1171 'post_name' => $form->options['old_style'],
1172 'post_type' => FrmStylesController::$post_type,
1173 );
1174
1175 $select = 'ID';
1176 $cache_key = FrmDb::generate_cache_key( $where, array( 'limit' => 1 ), $select, 'var' );
1177 FrmDb::delete_cache_and_transient( $cache_key, 'post' );
1178 }
1179 }
1180
1181 /**
1182 * Replace old form ids with new ones in a string.
1183 *
1184 * @param string $string
1185 * @param array<int> $form_ids new form ids indexed by old form id.
1186 *
1187 * @return string
1188 */
1189 private static function switch_form_ids( $string, $form_ids ) {
1190 if ( ! str_contains( $string, '[formidable' ) ) {
1191 // Skip string replacing if there are no form shortcodes in string.
1192 return $string;
1193 }
1194
1195 foreach ( $form_ids as $old_id => $new_id ) {
1196 $string = str_replace(
1197 array(
1198 '[formidable id="' . $old_id . '"',
1199 '[formidable id=' . $old_id . ']',
1200 '[formidable id=' . $old_id . ' ',
1201 '"formId":"' . $old_id . '"',
1202 ),
1203 array(
1204 '[formidable id="' . $new_id . '"',
1205 '[formidable id=' . $new_id . ']',
1206 '[formidable id=' . $new_id . ' ',
1207 '"formId":"' . $new_id . '"',
1208 ),
1209 $string
1210 );
1211 }
1212
1213 return $string;
1214 }
1215
1216 /**
1217 * @param array<array> $posts_with_shortcodes indexed by current post id.
1218 * @param array<int> $view_ids new view ids indexed by old view id.
1219 *
1220 * @return void
1221 */
1222 private static function maybe_switch_view_ids_after_importing_posts( $posts_with_shortcodes, $view_ids ) {
1223 foreach ( $posts_with_shortcodes as $imported_post_id => $post ) {
1224 $post_content = self::switch_view_ids( $post['post_content'], $view_ids );
1225
1226 if ( $post_content === $post['post_content'] ) {
1227 continue;
1228 }
1229
1230 wp_update_post(
1231 array(
1232 'ID' => $imported_post_id,
1233 'post_content' => $post_content,
1234 )
1235 );
1236 }
1237 }
1238
1239 /**
1240 * Replace old view ids with new ones in a string.
1241 *
1242 * @param string $string
1243 * @param array<int> $view_ids new view ids indexed by old view id.
1244 *
1245 * @return string
1246 */
1247 private static function switch_view_ids( $string, $view_ids ) {
1248 if ( ! str_contains( $string, '[display-frm-data' ) ) {
1249 // Skip string replacing if there are no view shortcodes in string.
1250 return $string;
1251 }
1252
1253 foreach ( $view_ids as $old_id => $new_id ) {
1254 $string = str_replace(
1255 array(
1256 '[display-frm-data id="' . $old_id . '"',
1257 '[display-frm-data id=' . $old_id . ']',
1258 '[display-frm-data id=' . $old_id . ' ',
1259 '"viewId":"' . $old_id . '"',
1260 ),
1261 array(
1262 '[display-frm-data id="' . $new_id . '"',
1263 '[display-frm-data id=' . $new_id . ']',
1264 '[display-frm-data id=' . $new_id . ' ',
1265 '"viewId":"' . $new_id . '"',
1266 ),
1267 $string
1268 );
1269 unset( $old_id, $new_id );
1270 }
1271
1272 return $string;
1273 }
1274
1275 /**
1276 * @param string $content
1277 *
1278 * @return string
1279 */
1280 private static function maybe_prepare_json_view_content( $content ) {
1281 $maybe_decoded = FrmAppHelper::maybe_json_decode( $content );
1282
1283 if ( is_array( $maybe_decoded ) && isset( $maybe_decoded[0] ) && isset( $maybe_decoded[0]['box'] ) ) {
1284 return FrmAppHelper::prepare_and_encode( $maybe_decoded );
1285 }
1286
1287 return $content;
1288 }
1289
1290 /**
1291 * @param array $post
1292 * @param object $item
1293 * @param array $imported
1294 *
1295 * @return void
1296 */
1297 private static function populate_post( &$post, $item, $imported ) {
1298 if ( isset( $item->attachment_url ) ) {
1299 $post['attachment_url'] = (string) $item->attachment_url;
1300 }
1301
1302 if ( $post['post_type'] === FrmFormActionsController::$action_post_type && isset( $imported['forms'][ (int) $post['menu_order'] ] ) ) {
1303 // Update to new form id
1304 $post['menu_order'] = $imported['forms'][ (int) $post['menu_order'] ];
1305 }
1306
1307 // Don't allow default styles to take over a site's default style
1308 if ( 'frm_styles' === $post['post_type'] ) {
1309 $post['menu_order'] = 0;
1310 }
1311
1312 foreach ( $item->postmeta as $meta ) {
1313 self::populate_postmeta( $post, $meta, $imported );
1314 unset( $meta );
1315 }
1316
1317 foreach ( $item->layout as $layout ) {
1318 self::populate_layout( $post, $layout );
1319 unset( $layout );
1320 }
1321
1322 self::populate_taxonomies( $post, $item );
1323
1324 self::maybe_editing_post( $post );
1325 }
1326
1327 /**
1328 * @param array $post
1329 * @param stdClass $meta
1330 * @param array $imported
1331 */
1332 private static function populate_postmeta( &$post, $meta, $imported ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh, Generic.Metrics.CyclomaticComplexity.MaxExceeded, SlevomatCodingStandard.Files.LineLength.LineTooLong
1333 global $frm_duplicate_ids;
1334
1335 $m = array(
1336 'key' => (string) $meta->meta_key,
1337 'value' => (string) $meta->meta_value,
1338 );
1339
1340 // Switch old form and field ids to new ones.
1341 if ( 'frm_form_id' === $m['key'] && isset( $imported['forms'][ (int) $m['value'] ] ) ) {
1342 $m['value'] = $imported['forms'][ (int) $m['value'] ];
1343 } else {
1344 $m['value'] = FrmAppHelper::maybe_json_decode( $m['value'] );
1345
1346 if ( $frm_duplicate_ids ) {
1347 if ( 'frm_dyncontent' === $m['key'] ) {
1348 $m['value'] = self::maybe_prepare_json_view_content( $m['value'] );
1349 $m['value'] = FrmFieldsHelper::switch_field_ids( $m['value'] );
1350 } elseif ( 'frm_options' === $m['key'] ) {
1351 foreach ( array( 'date_field_id', 'edate_field_id' ) as $setting_name ) {
1352 if ( isset( $m['value'][ $setting_name ] ) && is_numeric( $m['value'][ $setting_name ] ) && isset( $frm_duplicate_ids[ $m['value'][ $setting_name ] ] ) ) {
1353 $m['value'][ $setting_name ] = $frm_duplicate_ids[ $m['value'][ $setting_name ] ];
1354 }
1355 }
1356
1357 if ( ! empty( $m['value']['map_address_fields'] ) ) {
1358 foreach ( $m['value']['map_address_fields'] as $address_field_key => $address_field_id ) {
1359 if ( isset( $frm_duplicate_ids[ $address_field_id ] ) ) {
1360 $m['value']['map_address_fields'][ $address_field_key ] = $frm_duplicate_ids[ $address_field_id ];
1361 }
1362 }
1363 }
1364
1365 if ( ! empty( $m['value']['calendar_options'] ) ) {
1366 foreach ( $m['value']['calendar_options'] as $calendar_option_group_key => $calendar_option ) {
1367 if ( isset( $frm_duplicate_ids[ $calendar_option['value'] ] ) ) {
1368 $m['value']['calendar_options'][ $calendar_option_group_key ]['value'] = $frm_duplicate_ids[ $calendar_option['value'] ];
1369 }
1370 }
1371 }
1372
1373 if ( ! empty( $m['value']['timeline_options'] ) ) {
1374 foreach ( $m['value']['timeline_options'] as $timeline_option_group_key => $timeline_group_option ) {
1375 foreach ( $timeline_group_option as $timeline_option_key => $timeline_option ) {
1376 // @mago-expect lint:excessive-nesting
1377 if ( isset( $frm_duplicate_ids[ $timeline_option ] ) ) {
1378 $m['value']['timeline_options'][ $timeline_option_group_key ][ $timeline_option_key ] = $frm_duplicate_ids[ $timeline_option ];
1379 }
1380 }
1381 }
1382 }
1383
1384 $check_dup_array = array();
1385
1386 if ( ! empty( $m['value']['order_by'] ) ) {
1387 if ( is_numeric( $m['value']['order_by'] ) && isset( $frm_duplicate_ids[ $m['value']['order_by'] ] ) ) {
1388 $m['value']['order_by'] = $frm_duplicate_ids[ $m['value']['order_by'] ];
1389 } elseif ( is_array( $m['value']['order_by'] ) ) {
1390 $check_dup_array[] = 'order_by';
1391 }
1392 }
1393
1394 if ( ! empty( $m['value']['where'] ) ) {
1395 $check_dup_array[] = 'where';
1396 }
1397
1398 foreach ( $check_dup_array as $check_k ) {
1399 foreach ( (array) $m['value'][ $check_k ] as $mk => $mv ) {
1400 if ( isset( $frm_duplicate_ids[ $mv ] ) ) {
1401 $m['value'][ $check_k ][ $mk ] = $frm_duplicate_ids[ $mv ];
1402 }
1403 unset( $mk, $mv );
1404 }
1405 }
1406 }//end if
1407 }//end if
1408 }//end if
1409
1410 if ( ! is_array( $m['value'] ) ) {
1411 $m['value'] = FrmAppHelper::maybe_json_decode( $m['value'] );
1412 }
1413
1414 $post['postmeta'][ (string) $meta->meta_key ] = $m['value'];
1415 }
1416
1417 /**
1418 * @param array $post
1419 * @param object $layout
1420 *
1421 * @return void
1422 */
1423 private static function populate_layout( &$post, $layout ) {
1424 $post['layout'][ (string) $layout->type ] = (string) $layout->data;
1425 }
1426
1427 /**
1428 * Add terms to post
1429 *
1430 * @param array $post By reference.
1431 * @param object $item The XML object data.
1432 */
1433 private static function populate_taxonomies( &$post, $item ) {
1434 foreach ( $item->category as $c ) {
1435 $att = $c->attributes();
1436
1437 if ( ! isset( $att['nicename'] ) ) {
1438 continue;
1439 }
1440
1441 $taxonomy = (string) $att['domain'];
1442
1443 if ( is_taxonomy_hierarchical( $taxonomy ) ) {
1444 $name = (string) $att['nicename'];
1445 $h_term = get_term_by( 'slug', $name, $taxonomy );
1446
1447 if ( $h_term ) {
1448 $name = $h_term->term_id;
1449 }
1450 unset( $h_term );
1451 } else {
1452 $name = (string) $c;
1453 }
1454
1455 if ( ! isset( $post['tax_input'][ $taxonomy ] ) ) {
1456 $post['tax_input'][ $taxonomy ] = array();
1457 }
1458
1459 $post['tax_input'][ $taxonomy ][] = $name;
1460 unset( $name );
1461 }//end foreach
1462 }
1463
1464 /**
1465 * Edit post if the key and created time match.
1466 *
1467 * @param array $post By reference.
1468 *
1469 * @return void
1470 */
1471 private static function maybe_editing_post( &$post ) {
1472 $match_by = array(
1473 'post_type' => $post['post_type'],
1474 'name' => $post['post_name'],
1475 'post_status' => $post['post_status'],
1476 'posts_per_page' => 1,
1477 );
1478
1479 if ( in_array( $post['post_status'], array( 'trash', 'draft' ), true ) ) {
1480 $match_by['include'] = $post['post_id'];
1481 unset( $match_by['name'] );
1482 }
1483
1484 $editing = get_posts( $match_by );
1485
1486 // phpcs:ignore Universal.Operators.StrictComparisons
1487 if ( $editing && current( $editing )->post_date == $post['post_date'] ) {
1488 // Set the id of the post to edit
1489 $post['ID'] = current( $editing )->ID;
1490 }
1491 }
1492
1493 /**
1494 * @param array $post
1495 * @param int $post_id
1496 *
1497 * @return void
1498 */
1499 private static function update_postmeta( &$post, $post_id ) {
1500 foreach ( $post['postmeta'] as $k => $v ) {
1501 switch ( $k ) {
1502 case '_edit_last':
1503 $v = FrmAppHelper::get_user_id_param( $v );
1504 break;
1505
1506 case '_thumbnail_id':
1507 if ( FrmAppHelper::pro_is_installed() ) {
1508 // Change the attachment ID.
1509 $field_obj = FrmFieldFactory::get_field_type( 'file' );
1510 $v = $field_obj->get_file_id( $v );
1511 }
1512 break;
1513
1514 case 'frm_dyncontent':
1515 if ( is_array( $v ) ) {
1516 $v = json_encode( $v );
1517 }
1518 break;
1519
1520 case 'frm_param':
1521 add_rewrite_endpoint( $v, EP_PERMALINK | EP_PAGES );
1522 break;
1523 }//end switch
1524
1525 update_post_meta( $post_id, $k, $v );
1526 unset( $k, $v );
1527 }//end foreach
1528 }
1529
1530 /**
1531 * @param array $post
1532 * @param int $post_id
1533 */
1534 private static function update_layout( &$post, $post_id ) {
1535 if ( ! is_callable( 'FrmViewsLayout::maybe_create_layouts_for_view' ) ) {
1536 return;
1537 }
1538
1539 $listing_layout = ! empty( $post['layout']['listing'] ) ? json_decode( $post['layout']['listing'], true ) : array();
1540 $detail_layout = ! empty( $post['layout']['detail'] ) ? json_decode( $post['layout']['detail'], true ) : array();
1541
1542 if ( $listing_layout || $detail_layout ) {
1543 FrmViewsLayout::maybe_create_layouts_for_view( $post_id, $listing_layout, $detail_layout );
1544 }
1545 }
1546
1547 /**
1548 * @param array $imported
1549 *
1550 * @return void
1551 */
1552 private static function maybe_update_stylesheet( $imported ) {
1553 $new_styles = ! empty( $imported['imported']['styles'] );
1554 $updated_styles = ! empty( $imported['updated']['styles'] );
1555
1556 if ( ! $new_styles && ! $updated_styles ) {
1557 return;
1558 }
1559
1560 if ( is_admin() && function_exists( 'get_filesystem_method' ) ) {
1561 $frm_style = new FrmStyle();
1562 $frm_style->update( 'default' );
1563 }
1564
1565 foreach ( $imported['forms'] as $form_id ) {
1566 self::update_custom_style_setting_after_import( $form_id );
1567 }
1568 }
1569
1570 /**
1571 * @param mixed $result
1572 * @param string $message
1573 * @param array $errors
1574 *
1575 * @return void
1576 */
1577 public static function parse_message( $result, &$message, &$errors ) {
1578 if ( is_wp_error( $result ) ) {
1579 $errors[] = $result->get_error_message();
1580
1581 // Remove the SimpleXML_parse_error from the WP_Error object to avoid
1582 // displaying duplicate error messages from $result->get_error_message()
1583 $error_details = array();
1584
1585 foreach ( $result->get_error_codes() as $error_code ) {
1586 // Clone WP_Error data because WP_Error removes all error messages and data
1587 // Associated with the specified error code when an item is removed.
1588 // Source: https://developer.wordpress.org/reference/classes/wp_error/remove/#source
1589 $error_details = $result->get_error_data( $error_code );
1590
1591 if ( $error_code === 'SimpleXML_parse_error' ) {
1592 $result->remove( $error_code );
1593 break;
1594 }
1595 }
1596
1597 if ( $error_details ) {
1598 $errors[] = '<br />' . esc_html_x( 'Error details:', 'import xml message', 'formidable' ) . '<br />' . esc_html( print_r( $error_details, 1 ) );
1599 }
1600
1601 return;
1602 }//end if
1603
1604 if ( ! $result ) {
1605 return;
1606 }
1607
1608 if ( ! is_array( $result ) ) {
1609 $message = is_string( $result ) ? $result : htmlentities( print_r( $result, 1 ) );
1610 return;
1611 }
1612
1613 $t_strings = array(
1614 'imported' => __( 'Imported', 'formidable' ),
1615 'updated' => __( 'Updated', 'formidable' ),
1616 );
1617
1618 $message = '<ul>';
1619
1620 foreach ( $result as $type => $results ) {
1621 if ( ! isset( $t_strings[ $type ] ) ) {
1622 // Only print imported and updated
1623 continue;
1624 }
1625
1626 $s_message = array();
1627
1628 foreach ( $results as $k => $m ) {
1629 self::item_count_message( $m, $k, $s_message );
1630 unset( $k, $m );
1631 }
1632
1633 if ( ! $s_message ) {
1634 continue;
1635 }
1636
1637 $message .= '<li><strong>' . $t_strings[ $type ] . ':</strong> ';
1638 $message .= implode( ', ', $s_message );
1639 $message .= '</li>';
1640 }//end foreach
1641
1642 if ( $message === '<ul>' ) {
1643 $message = '';
1644 $errors[] = __( 'Nothing was imported or updated', 'formidable' );
1645
1646 return;
1647 }
1648
1649 self::add_form_link_to_message( $result, $message );
1650
1651 /**
1652 * @since 5.3
1653 *
1654 * @param string $message
1655 * @param array $result
1656 */
1657 $message = apply_filters( 'frm_xml_parsed_message', $message, $result );
1658 $message .= '</ul>';
1659 }
1660
1661 /**
1662 * @param int $m
1663 * @param string $type
1664 * @param array<string> $s_message
1665 *
1666 * @return void
1667 */
1668 public static function item_count_message( $m, $type, &$s_message ) {
1669 if ( ! $m ) {
1670 return;
1671 }
1672
1673 $strings = array(
1674 /* translators: %1$s: Number of items */
1675 'forms' => sprintf( _n( '%1$s Form', '%1$s Forms', $m, 'formidable' ), $m ),
1676 /* translators: %1$s: Number of items */
1677 'fields' => sprintf( _n( '%1$s Field', '%1$s Fields', $m, 'formidable' ), $m ),
1678 /* translators: %1$s: Number of items */
1679 'items' => sprintf( _n( '%1$s Entry', '%1$s Entries', $m, 'formidable' ), $m ),
1680 /* translators: %1$s: Number of items */
1681 'views' => sprintf( _n( '%1$s View', '%1$s Views', $m, 'formidable' ), $m ),
1682 /* translators: %1$s: Number of items */
1683 'posts' => sprintf( _n( '%1$s Page/Post', '%1$s Pages/Posts', $m, 'formidable' ), $m ),
1684 /* translators: %1$s: Number of items */
1685 'styles' => sprintf( _n( '%1$s Style', '%1$s Styles', $m, 'formidable' ), $m ),
1686 /* translators: %1$s: Number of items */
1687 'terms' => sprintf( _n( '%1$s Term', '%1$s Terms', $m, 'formidable' ), $m ),
1688 /* translators: %1$s: Number of items */
1689 'actions' => sprintf( _n( '%1$s Form Action', '%1$s Form Actions', $m, 'formidable' ), $m ),
1690 );
1691
1692 if ( isset( $strings[ $type ] ) ) {
1693 $s_message[] = $strings[ $type ];
1694 return;
1695 }
1696
1697 $string = ' ' . $m . ' ' . ucfirst( $type );
1698
1699 /**
1700 * @since 5.3
1701 *
1702 * @param string $string Message string for imported item.
1703 * @param int $m Number of item that was imported.
1704 * }
1705 */
1706 $string = apply_filters( 'frm_xml_' . $type . '_count_message', $string, $m );
1707 $s_message[] = $string;
1708 }
1709
1710 /**
1711 * If a single form was imported, include a link in the success message.
1712 *
1713 * @since 4.0
1714 *
1715 * @param array $result The response from the XML import.
1716 * @param string $message The response shown on the page after import.
1717 */
1718 private static function add_form_link_to_message( $result, &$message ) {
1719 $total_forms = $result['imported']['forms'] + $result['updated']['forms'];
1720
1721 if ( $total_forms > 1 ) {
1722 return;
1723 }
1724
1725 $primary_form = reset( $result['forms'] );
1726
1727 if ( ! $primary_form ) {
1728 return;
1729 }
1730
1731 $primary_form = FrmForm::getOne( $primary_form );
1732 $form_id = ! empty( $primary_form->parent_form_id ) ? $primary_form->parent_form_id : $primary_form->id;
1733 $message .= '<li><a href="' . esc_url( FrmForm::get_edit_link( $form_id ) ) . '">' . esc_html__( 'Go to imported form', 'formidable' ) . '</a></li>';
1734 }
1735
1736 /**
1737 * Prepare the form options for export
1738 *
1739 * @since 2.0.19
1740 *
1741 * @param string $options
1742 *
1743 * @return string
1744 */
1745 public static function prepare_form_options_for_export( $options ) {
1746 FrmAppHelper::unserialize_or_decode( $options );
1747 // Change custom_style to the post_name instead of ID (1 may be a string)
1748 $not_default = isset( $options['custom_style'] ) && 1 != $options['custom_style']; // phpcs:ignore Universal.Operators.StrictComparisons
1749
1750 if ( $not_default ) {
1751 global $wpdb;
1752 $table = $wpdb->prefix . 'posts';
1753 $where = array( 'ID' => $options['custom_style'] );
1754 $select = 'post_name';
1755 $style_name = FrmDb::get_var( $table, $where, $select );
1756 $options['custom_style'] = $style_name ? $style_name : 1;
1757 }
1758 self::remove_default_form_options( $options );
1759 $options = serialize( $options );
1760
1761 return self::cdata( $options );
1762 }
1763
1764 /**
1765 * If the saved value is the same as the default, remove it from the export
1766 * This keeps file size down and prevents overriding global settings after import
1767 *
1768 * @since 3.06
1769 *
1770 * @param array $options By reference.
1771 *
1772 * @return void
1773 */
1774 private static function remove_default_form_options( &$options ) {
1775 $defaults = FrmFormsHelper::get_default_opts();
1776
1777 if ( is_callable( 'FrmProFormsHelper::get_default_opts' ) ) {
1778 $defaults += FrmProFormsHelper::get_default_opts();
1779 }
1780 self::remove_defaults( $defaults, $options );
1781 }
1782
1783 /**
1784 * Remove extra settings from field to keep file size down
1785 *
1786 * @since 3.06
1787 *
1788 * @param object $field
1789 *
1790 * @return void
1791 */
1792 public static function prepare_field_for_export( &$field ) {
1793 self::remove_default_field_options( $field );
1794 self::add_image_src_to_image_options( $field );
1795 }
1796
1797 /**
1798 * Remove defaults from field options too
1799 *
1800 * @since 3.06
1801 *
1802 * @param object $field
1803 *
1804 * @return void
1805 */
1806 private static function remove_default_field_options( &$field ) {
1807 $defaults = self::default_field_options( $field->type );
1808
1809 if ( empty( $defaults['blank'] ) ) {
1810 $global_settings = new FrmSettings();
1811 $global_defaults = $global_settings->default_options();
1812 $defaults['blank'] = $global_defaults['blank_msg'];
1813 }
1814
1815 $options = $field->field_options;
1816 FrmAppHelper::unserialize_or_decode( $options );
1817 self::remove_defaults( $defaults, $options );
1818 self::remove_default_html( 'custom_html', $defaults, $options );
1819
1820 // Get variations on the defaults.
1821 if ( isset( $options['invalid'] ) ) {
1822 $defaults = array(
1823 /* translators: %s: Field name */
1824 'invalid' => sprintf( __( '%s is invalid', 'formidable' ), $field->name ),
1825 );
1826 self::remove_defaults( $defaults, $options );
1827 }
1828
1829 $field->field_options = serialize( $options );
1830 }
1831
1832 /**
1833 * Add image "src" key to each image option so the image can be imported to another website.
1834 *
1835 * @since 5.5.1
1836 *
1837 * @param stdClass $field
1838 *
1839 * @return void
1840 */
1841 private static function add_image_src_to_image_options( $field ) {
1842 if ( empty( $field->options ) || ! str_contains( $field->options, 'image' ) ) {
1843 return;
1844 }
1845
1846 $updated = false;
1847 $options = $field->options;
1848 FrmAppHelper::unserialize_or_decode( $options );
1849
1850 if ( ! $options || ! is_array( $options ) ) {
1851 return;
1852 }
1853
1854 foreach ( $options as $key => $option ) {
1855 if ( ! is_array( $option ) || empty( $option['image'] ) ) {
1856 continue;
1857 }
1858
1859 $options[ $key ]['src'] = wp_get_attachment_url( $option['image'] );
1860 $updated = true;
1861 }
1862
1863 if ( $updated ) {
1864 $field->options = maybe_serialize( $options );
1865 }
1866 }
1867
1868 /**
1869 * @since 3.06.03
1870 *
1871 * @param string $type
1872 *
1873 * @return array
1874 */
1875 private static function default_field_options( $type ) {
1876 $defaults = FrmFieldsHelper::get_default_field_options( $type );
1877
1878 if ( empty( $defaults['custom_html'] ) ) {
1879 $defaults['custom_html'] = FrmFieldsHelper::get_default_html( $type );
1880 }
1881
1882 return $defaults;
1883 }
1884
1885 /**
1886 * Compare the default array to the saved values and
1887 * remove if they are the same
1888 *
1889 * @since 3.06
1890 *
1891 * @param array $defaults
1892 * @param array $saved
1893 *
1894 * @return void
1895 */
1896 private static function remove_defaults( $defaults, &$saved ) {
1897 foreach ( $saved as $key => $value ) {
1898 if ( isset( $defaults[ $key ] ) && $defaults[ $key ] === $value ) {
1899 unset( $saved[ $key ] );
1900 }
1901 }
1902 }
1903
1904 /**
1905 * The line endings may prevent html from being equal when it should
1906 *
1907 * @since 3.06
1908 *
1909 * @param string $html_name
1910 * @param array $defaults
1911 * @param array $options
1912 *
1913 * @return void
1914 */
1915 private static function remove_default_html( $html_name, $defaults, &$options ) {
1916 if ( ! isset( $options[ $html_name ] ) || ! isset( $defaults[ $html_name ] ) ) {
1917 return;
1918 }
1919
1920 $old_html = str_replace( "\r\n", "\n", $options[ $html_name ] );
1921 $default_html = $defaults[ $html_name ];
1922
1923 // phpcs:ignore Universal.Operators.StrictComparisons
1924 if ( $old_html == $default_html ) {
1925 unset( $options[ $html_name ] );
1926 return;
1927 }
1928
1929 // Account for some of the older field default HTML.
1930 $default_html = str_replace( ' id="frm_desc_field_[key]"', '', $default_html );
1931
1932 if ( $old_html === $default_html ) {
1933 unset( $options[ $html_name ] );
1934 }
1935 }
1936
1937 /**
1938 * @param string $str
1939 *
1940 * @return string
1941 */
1942 public static function cdata( $str ) {
1943 FrmAppHelper::unserialize_or_decode( $str );
1944
1945 if ( is_array( $str ) ) {
1946 $str = json_encode( $str );
1947 } elseif ( FrmAppHelper::is_valid_utf8( $str ) === false ) {
1948 $str = FrmAppHelper::maybe_utf8_encode( $str );
1949 }
1950
1951 if ( is_numeric( $str ) ) {
1952 return $str;
1953 }
1954
1955 self::remove_invalid_characters_from_xml( $str );
1956
1957 // $str = ent2ncr(esc_html( $str));
1958 return '<![CDATA[' . str_replace( ']]>', ']]]]><![CDATA[>', $str ) . ']]>';
1959 }
1960
1961 /**
1962 * Remove <US> character (unit separator) from exported strings
1963 *
1964 * @since 2.0.22
1965 *
1966 * @param string $str
1967 *
1968 * @return void
1969 */
1970 private static function remove_invalid_characters_from_xml( &$str ) {
1971 // Remove <US> character
1972 $str = str_replace( '\x1F', '', $str );
1973 }
1974
1975 /**
1976 * Migrate form settings to actions
1977 *
1978 * @param array $form_options
1979 * @param int $form_id
1980 * @param array $imported
1981 * @param bool $switch
1982 *
1983 * @return void
1984 */
1985 public static function migrate_form_settings_to_actions( $form_options, $form_id, &$imported = array(), $switch = false ) {
1986 // Get post type
1987 $post_type = FrmFormActionsController::$action_post_type;
1988
1989 // Set up imported index, if not set up yet
1990 if ( ! isset( $imported['imported']['actions'] ) ) {
1991 $imported['imported']['actions'] = 0;
1992 }
1993
1994 // Migrate post settings to action
1995 self::migrate_post_settings_to_action( $form_options, $form_id, $post_type, $imported, $switch );
1996
1997 // Migrate email settings to action
1998 self::migrate_email_settings_to_action( $form_options, $form_id, $post_type, $imported, $switch );
1999 }
2000
2001 /**
2002 * Migrate post settings to form action
2003 *
2004 * @param array $form_options
2005 * @param int $form_id
2006 * @param string $post_type
2007 * @param array $imported
2008 * @param bool $switch
2009 */
2010 private static function migrate_post_settings_to_action( $form_options, $form_id, $post_type, &$imported, $switch ) {
2011 if ( empty( $form_options['create_post'] ) ) {
2012 return;
2013 }
2014
2015 $new_action = array(
2016 'post_type' => $post_type,
2017 'post_excerpt' => 'wppost',
2018 'post_title' => __( 'Create Posts', 'formidable' ),
2019 'menu_order' => $form_id,
2020 'post_status' => 'publish',
2021 'post_content' => array(),
2022 'post_name' => $form_id . '_wppost_1',
2023 );
2024
2025 $post_settings = array(
2026 'post_type',
2027 'post_category',
2028 'post_content',
2029 'post_excerpt',
2030 'post_title',
2031 'post_name',
2032 'post_date',
2033 'post_status',
2034 'post_custom_fields',
2035 'post_password',
2036 'post_parent',
2037 );
2038
2039 foreach ( $post_settings as $post_setting ) {
2040 if ( isset( $form_options[ $post_setting ] ) ) {
2041 $new_action['post_content'][ $post_setting ] = $form_options[ $post_setting ];
2042 }
2043 unset( $post_setting );
2044 }
2045
2046 $new_action['event'] = array( 'create', 'update' );
2047
2048 if ( $switch ) {
2049 // Fields with string or int saved.
2050 $basic_fields = array(
2051 'post_title',
2052 'post_content',
2053 'post_excerpt',
2054 'post_password',
2055 'post_date',
2056 'post_status',
2057 'post_parent',
2058 );
2059
2060 // Fields with arrays saved.
2061 $array_fields = array( 'post_category', 'post_custom_fields' );
2062
2063 $new_action['post_content'] = self::switch_action_field_ids( $new_action['post_content'], $basic_fields, $array_fields );
2064 }
2065
2066 $new_action['post_content'] = json_encode( $new_action['post_content'] );
2067
2068 $exists = get_posts(
2069 array(
2070 'name' => $new_action['post_name'],
2071 'post_type' => $new_action['post_type'],
2072 'post_status' => $new_action['post_status'],
2073 'numberposts' => 1,
2074 )
2075 );
2076
2077 if ( $exists ) {
2078 return;
2079 }
2080
2081 // This isn't an email, but we need to use a class that will always be included
2082 FrmDb::save_json_post( $new_action );
2083 ++$imported['imported']['actions'];
2084 }
2085
2086 /**
2087 * Switch old field IDs for new field IDs in emails and post
2088 *
2089 * @since 2.0
2090 *
2091 * @param array $post_content Check for old field IDs.
2092 * @param array $basic_fields Fields with string or int saved.
2093 * @param array $array_fields Fields with arrays saved.
2094 *
2095 * @return string $post_content - new field IDs
2096 */
2097 private static function switch_action_field_ids( $post_content, $basic_fields, $array_fields = array() ) {
2098 global $frm_duplicate_ids;
2099
2100 // If there aren't IDs that were switched, end now
2101 if ( ! $frm_duplicate_ids ) {
2102 return null;
2103 }
2104
2105 // Get old IDs
2106 $old = array_keys( $frm_duplicate_ids );
2107
2108 // Get new IDs
2109 $new = array_values( $frm_duplicate_ids );
2110
2111 // Do a str_replace with each item to set the new IDs
2112 foreach ( $post_content as $key => $setting ) {
2113 // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
2114 if ( ! is_array( $setting ) && in_array( $key, $basic_fields ) ) {
2115 // Replace old IDs with new IDs
2116 $post_content[ $key ] = str_replace( $old, $new, $setting );
2117 // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
2118 } elseif ( is_array( $setting ) && in_array( $key, $array_fields ) ) {
2119 foreach ( $setting as $k => $val ) {
2120 // Replace old IDs with new IDs
2121 $post_content[ $key ][ $k ] = str_replace( $old, $new, $val );
2122 }
2123 }
2124 unset( $key, $setting );
2125 }
2126
2127 return $post_content;
2128 }
2129
2130 /**
2131 * Migrate email settings to action
2132 *
2133 * @param array $form_options
2134 * @param int $form_id
2135 * @param string $post_type
2136 * @param array $imported
2137 * @param bool $switch
2138 *
2139 * @return void
2140 */
2141 private static function migrate_email_settings_to_action( $form_options, $form_id, $post_type, &$imported, $switch ) {
2142 // No old notifications or autoresponders to carry over
2143 if ( ! isset( $form_options['auto_responder'] ) && ! isset( $form_options['notification'] ) && ! isset( $form_options['email_to'] ) ) {
2144 return;
2145 }
2146
2147 // Initialize notifications array
2148 $notifications = array();
2149
2150 // Migrate regular notifications
2151 self::migrate_notifications_to_action( $form_options, $form_id, $notifications );
2152
2153 // Migrate autoresponders
2154 self::migrate_autoresponder_to_action( $form_options, $form_id, $notifications );
2155
2156 if ( ! $notifications ) {
2157 return;
2158 }
2159
2160 foreach ( $notifications as $new_notification ) {
2161 $new_notification['post_type'] = $post_type;
2162 $new_notification['post_excerpt'] = 'email';
2163 $new_notification['post_title'] = __( 'Email Notification', 'formidable' );
2164 $new_notification['menu_order'] = $form_id;
2165 $new_notification['post_status'] = 'publish';
2166
2167 // Switch field IDs and keys, if needed
2168 if ( $switch ) {
2169 // Switch field IDs in email conditional logic
2170 self::switch_email_condition_field_ids( $new_notification['post_content'] );
2171
2172 // Switch all other field IDs in email
2173 $new_notification['post_content'] = FrmFieldsHelper::switch_field_ids( $new_notification['post_content'] );
2174 }
2175
2176 $new_notification['post_content'] = FrmAppHelper::prepare_and_encode( $new_notification['post_content'] );
2177
2178 $exists = get_posts(
2179 array(
2180 'name' => $new_notification['post_name'],
2181 'post_type' => $new_notification['post_type'],
2182 'post_status' => $new_notification['post_status'],
2183 'numberposts' => 1,
2184 )
2185 );
2186
2187 if ( ! $exists ) {
2188 FrmDb::save_json_post( $new_notification );
2189 ++$imported['imported']['actions'];
2190 }
2191 unset( $new_notification );
2192 }//end foreach
2193
2194 self::remove_deprecated_notification_settings( $form_id, $form_options );
2195 }
2196
2197 /**
2198 * Remove deprecated notification settings after migration
2199 *
2200 * @since 2.05
2201 *
2202 * @param int|string $form_id
2203 * @param array $form_options
2204 *
2205 * @return void
2206 */
2207 private static function remove_deprecated_notification_settings( $form_id, $form_options ) {
2208 $delete_settings = array( 'notification', 'autoresponder', 'email_to' );
2209
2210 foreach ( $delete_settings as $index ) {
2211 if ( isset( $form_options[ $index ] ) ) {
2212 unset( $form_options[ $index ] );
2213 }
2214 }
2215
2216 FrmForm::update( $form_id, array( 'options' => $form_options ) );
2217 }
2218
2219 /**
2220 * Migrate notifications to action
2221 *
2222 * @param array $form_options
2223 * @param int $form_id
2224 * @param array $notifications
2225 *
2226 * @return void
2227 */
2228 private static function migrate_notifications_to_action( $form_options, $form_id, &$notifications ) {
2229 if ( ! isset( $form_options['notification'] ) && ! empty( $form_options['email_to'] ) ) {
2230 // Add old settings into notification array
2231 $form_options['notification'] = array( 0 => $form_options );
2232 } elseif ( isset( $form_options['notification']['email_to'] ) ) {
2233 // Make sure it's in the correct format
2234 $form_options['notification'] = array( 0 => $form_options['notification'] );
2235 }
2236
2237 if ( isset( $form_options['notification'] ) && is_array( $form_options['notification'] ) ) {
2238 foreach ( $form_options['notification'] as $email_key => $notification ) {
2239 $atts = array(
2240 'email_to' => '',
2241 'reply_to' => '',
2242 'reply_to_name' => '',
2243 'event' => '',
2244 'form_id' => $form_id,
2245 'email_key' => $email_key,
2246 );
2247
2248 // Format the email data
2249 self::format_email_data( $atts, $notification );
2250
2251 if ( ! empty( $notification['twilio'] ) ) {
2252 do_action( 'frm_create_twilio_action', $atts, $notification );
2253 }
2254
2255 // Setup the new notification
2256 $new_notification = array();
2257 self::setup_new_notification( $new_notification, $notification, $atts );
2258
2259 $notifications[] = $new_notification;
2260 }//end foreach
2261 }//end if
2262 }
2263
2264 /**
2265 * Format email data
2266 *
2267 * @param array $atts
2268 * @param array $notification
2269 *
2270 * @return void
2271 */
2272 private static function format_email_data( &$atts, $notification ) {
2273 // Format email_to
2274 self::format_email_to_data( $atts, $notification );
2275
2276 // Format the reply to email and name
2277 $reply_fields = array(
2278 'reply_to' => '',
2279 'reply_to_name' => '',
2280 );
2281
2282 foreach ( $reply_fields as $f => $val ) {
2283 if ( isset( $notification[ $f ] ) ) {
2284 $atts[ $f ] = $notification[ $f ];
2285
2286 if ( 'custom' === $notification[ $f ] ) {
2287 $atts[ $f ] = $notification[ 'cust_' . $f ];
2288 } elseif ( is_numeric( $atts[ $f ] ) && $atts[ $f ] ) {
2289 $atts[ $f ] = '[' . $atts[ $f ] . ']';
2290 }
2291 }
2292 unset( $f, $val );
2293 }
2294
2295 // Format event
2296 $atts['event'] = array( 'create' );
2297
2298 // phpcs:ignore Universal.Operators.StrictComparisons
2299 if ( isset( $notification['update_email'] ) && 1 == $notification['update_email'] ) {
2300 $atts['event'][] = 'update';
2301 } elseif ( isset( $notification['update_email'] ) && 2 == $notification['update_email'] ) { // phpcs:ignore Universal.Operators.StrictComparisons
2302 $atts['event'] = array( 'update' );
2303 }
2304 }
2305
2306 /**
2307 * Format email_to data
2308 *
2309 * @param array $atts
2310 * @param array $notification
2311 *
2312 * @return void
2313 */
2314 private static function format_email_to_data( &$atts, $notification ) {
2315 $atts['email_to'] = isset( $notification['email_to'] ) ? preg_split( '/ (,|;) /', $notification['email_to'] ) : array();
2316
2317 if ( isset( $notification['also_email_to'] ) ) {
2318 $email_fields = (array) $notification['also_email_to'];
2319 $atts['email_to'] = array_merge( $email_fields, $atts['email_to'] );
2320 unset( $email_fields );
2321 }
2322
2323 foreach ( $atts['email_to'] as $key => $email_field ) {
2324 if ( is_numeric( $email_field ) ) {
2325 $atts['email_to'][ $key ] = '[' . $email_field . ']';
2326 }
2327
2328 if ( ! str_contains( $email_field, '|' ) ) {
2329 continue;
2330 }
2331
2332 $email_opt = explode( '|', $email_field );
2333
2334 if ( isset( $email_opt[0] ) ) {
2335 $atts['email_to'][ $key ] = '[' . $email_opt[0] . ' show=' . $email_opt[1] . ']';
2336 }
2337 unset( $email_opt );
2338 }
2339
2340 $atts['email_to'] = implode( ', ', $atts['email_to'] );
2341 }
2342
2343 /**
2344 * Setup new notification
2345 *
2346 * @param array $new_notification
2347 * @param array $notification
2348 * @param array $atts
2349 *
2350 * @return void
2351 */
2352 private static function setup_new_notification( &$new_notification, $notification, $atts ) {
2353 // Set up new notification
2354 $new_notification = array(
2355 'post_content' => array(
2356 'email_to' => $atts['email_to'],
2357 'event' => $atts['event'],
2358 ),
2359 'post_name' => $atts['form_id'] . '_email_' . $atts['email_key'],
2360 );
2361
2362 // Add more fields to the new notification
2363 $add_fields = array( 'email_message', 'email_subject', 'plain_text', 'inc_user_info', 'conditions' );
2364
2365 foreach ( $add_fields as $add_field ) {
2366 if ( isset( $notification[ $add_field ] ) ) {
2367 $new_notification['post_content'][ $add_field ] = $notification[ $add_field ];
2368 } elseif ( in_array( $add_field, array( 'plain_text', 'inc_user_info' ), true ) ) {
2369 $new_notification['post_content'][ $add_field ] = 0;
2370 } else {
2371 $new_notification['post_content'][ $add_field ] = '';
2372 }
2373 unset( $add_field );
2374 }
2375
2376 // Set reply to
2377 $new_notification['post_content']['reply_to'] = $atts['reply_to'];
2378
2379 // Set from
2380 if ( ! empty( $atts['reply_to'] ) || ! empty( $atts['reply_to_name'] ) ) {
2381 $new_notification['post_content']['from'] = ( ! empty( $atts['reply_to_name'] ) ? $atts['reply_to_name'] : '[sitename]' ) . ' <' . ( ! empty( $atts['reply_to'] ) ? $atts['reply_to'] : '[admin_email]' ) . '>'; // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
2382 }
2383 }
2384
2385 /**
2386 * Switch field IDs in pre-2.0 email conditional logic
2387 *
2388 * @param array $post_content Pass by reference.
2389 *
2390 * @return void
2391 */
2392 private static function switch_email_condition_field_ids( &$post_content ) {
2393 // Switch field IDs in conditional logic
2394 if ( ! isset( $post_content['conditions'] ) || ! is_array( $post_content['conditions'] ) ) {
2395 return;
2396 }
2397
2398 foreach ( $post_content['conditions'] as $email_key => $val ) {
2399 if ( is_numeric( $email_key ) ) {
2400 $post_content['conditions'][ $email_key ] = self::switch_action_field_ids( $val, array( 'hide_field' ) );
2401 }
2402 unset( $email_key, $val );
2403 }
2404 }
2405
2406 /**
2407 * Migrate autoresponder to action
2408 *
2409 * @param array $form_options
2410 * @param int $form_id
2411 * @param array $notifications
2412 *
2413 * @return void
2414 */
2415 private static function migrate_autoresponder_to_action( $form_options, $form_id, &$notifications ) {
2416 if ( empty( $form_options['auto_responder'] ) || empty( $form_options['ar_email_message'] ) ) {
2417 return;
2418 }
2419
2420 // Migrate autoresponder
2421
2422 $email_field = $form_options['ar_email_to'] ?? 0;
2423
2424 if ( str_contains( $email_field, '|' ) ) {
2425 // Data from entries field
2426 $email_field = explode( '|', $email_field );
2427
2428 if ( isset( $email_field[1] ) ) {
2429 $email_field = $email_field[1];
2430 }
2431 }
2432
2433 if ( is_numeric( $email_field ) && $email_field ) {
2434 $email_field = '[' . $email_field . ']';
2435 }
2436
2437 $notification = $form_options;
2438 $new_notification2 = array(
2439 'post_content' => array(
2440 'email_message' => $notification['ar_email_message'],
2441 'email_subject' => $notification['ar_email_subject'] ?? '',
2442 'email_to' => $email_field,
2443 'plain_text' => $notification['ar_plain_text'] ?? 0,
2444 'inc_user_info' => 0,
2445 ),
2446 'post_name' => $form_id . '_email_' . count( $notifications ),
2447 );
2448
2449 $reply_to = $notification['ar_reply_to'] ?? '';
2450 $reply_to_name = $notification['ar_reply_to_name'] ?? '';
2451
2452 if ( $reply_to ) {
2453 $new_notification2['post_content']['reply_to'] = $reply_to;
2454 }
2455
2456 if ( $reply_to || $reply_to_name ) {
2457 $new_notification2['post_content']['from'] = ( $reply_to_name ? $reply_to_name : '[sitename]' ) . ' <' . ( $reply_to ? $reply_to : '[admin_email]' ) . '>'; // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
2458 }
2459
2460 $notifications[] = $new_notification2;
2461 unset( $new_notification2 );
2462 }
2463
2464 /**
2465 * PHP 8 backward compatibility for the libxml_disable_entity_loader function
2466 *
2467 * @param bool $loader
2468 *
2469 * @return bool
2470 */
2471 public static function maybe_libxml_disable_entity_loader( $loader ) {
2472 if ( version_compare( phpversion(), '8.0', '<' ) && function_exists( 'libxml_disable_entity_loader' ) ) {
2473 $loader = libxml_disable_entity_loader( $loader ); // phpcs:disable Generic.PHP.DeprecatedFunctions.Deprecated
2474 }
2475
2476 return $loader;
2477 }
2478
2479 /**
2480 * PHP 8 backward compatibility for the libxml_disable_entity_loader function
2481 *
2482 * @return bool
2483 */
2484 public static function check_if_libxml_disable_entity_loader_exists() {
2485 return version_compare( phpversion(), '8.0', '<' ) && ! function_exists( 'libxml_disable_entity_loader' );
2486 }
2487
2488 /**
2489 * Get the file types allowed for importing.
2490 * This is used in the "accept" attribute for the file upload input on the XML import page.
2491 *
2492 * @since 6.8.3
2493 *
2494 * @return array
2495 */
2496 public static function get_supported_upload_file_types() {
2497 $file_types = array( '.xml' );
2498
2499 if ( FrmAppHelper::pro_is_installed() ) {
2500 // CSV Importing is only available in Pro.
2501 $file_types[] = '.csv';
2502 }
2503
2504 return $file_types;
2505 }
2506 }
2507