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

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