PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.29
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.29
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.29, at classes/helpers/FrmXMLHelper.php

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