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

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