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

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