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 / models / FrmForm.php

FrmForm.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More trunk, at classes/models/FrmForm.php

1,401 lines 37.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 FrmForm {
7
8 /**
9 * @param array $values
10 *
11 * @return bool|int id on success or false on failure.
12 */
13 public static function create( $values ) {
14 global $wpdb;
15
16 $values = FrmAppHelper::maybe_filter_array( $values, array( 'name', 'description' ) );
17
18 $new_values = array(
19 'form_key' => FrmAppHelper::get_unique_key( $values['form_key'], $wpdb->prefix . 'frm_forms', 'form_key' ),
20 'name' => FrmAppHelper::truncate( $values['name'], 255, 1, '', true ),
21 'description' => $values['description'],
22 'status' => $values['status'] ?? 'published',
23 'logged_in' => $values['logged_in'] ?? 0,
24 'is_template' => isset( $values['is_template'] ) ? (int) $values['is_template'] : 0,
25 'parent_form_id' => isset( $values['parent_form_id'] ) ? absint( $values['parent_form_id'] ) : 0,
26 'editable' => isset( $values['editable'] ) ? (int) $values['editable'] : 0,
27 'created_at' => $values['created_at'] ?? current_time( 'mysql', 1 ),
28 );
29
30 $options = isset( $values['options'] ) ? (array) $values['options'] : array();
31 FrmFormsHelper::fill_form_options( $options, $values );
32
33 $options['before_html'] = $values['options']['before_html'] ?? FrmFormsHelper::get_default_html( 'before' );
34 $options['after_html'] = $values['options']['after_html'] ?? FrmFormsHelper::get_default_html( 'after' );
35 $options['submit_html'] = $values['options']['submit_html'] ?? FrmFormsHelper::get_default_html( 'submit' );
36
37 /**
38 * Allows modifying form options before updating or creating.
39 *
40 * @since 5.4 Add the third param.
41 *
42 * @param array $options Form options.
43 * @param array $values Form data.
44 * @param bool $update Is form updating or creating. It's `true` if is updating.
45 */
46 $options = apply_filters( 'frm_form_options_before_update', $options, $values, false );
47 $options = self::maybe_filter_form_options( $options );
48 $new_values['options'] = serialize( $options );
49
50 $wpdb->insert( $wpdb->prefix . 'frm_forms', $new_values );
51
52 $id = $wpdb->insert_id;
53
54 // Clear form caching
55 self::clear_form_cache();
56
57 return $id;
58 }
59
60 /**
61 * @since 5.0.08
62 *
63 * @param array $options
64 *
65 * @return array
66 */
67 private static function maybe_filter_form_options( $options ) {
68 if ( ! FrmAppHelper::allow_unfiltered_html() && ! empty( $options['submit_html'] ) ) {
69 $options['submit_html'] = FrmAppHelper::kses_submit_button( $options['submit_html'] );
70 }
71 return FrmAppHelper::maybe_filter_array( $options, array( 'submit_value', 'success_msg', 'before_html', 'after_html' ) );
72 }
73
74 /**
75 * Duplicate a form.
76 *
77 * @param int $id Form ID to duplicate.
78 * @param bool $template Whether the duplicated form is a template.
79 * @param bool $copy_keys Whether to copy the original form key.
80 * @param false|int $blog_id Blog ID when duplicating across sites, or false for current site.
81 *
82 * @return bool|int ID on success or false on failure
83 */
84 public static function duplicate( $id, $template = false, $copy_keys = false, $blog_id = false ) {
85 global $wpdb;
86
87 $values = self::getOne( $id, $blog_id );
88
89 if ( ! $values ) {
90 return false;
91 }
92
93 $new_key = $copy_keys ? $values->form_key : '';
94
95 $new_values = array(
96 'form_key' => FrmAppHelper::get_unique_key( $new_key, $wpdb->prefix . 'frm_forms', 'form_key' ),
97 'name' => $values->name,
98 'description' => $values->description,
99 'status' => $values->status ? $values->status : 'published',
100 'logged_in' => $values->logged_in ? $values->logged_in : 0,
101 'editable' => $values->editable ? $values->editable : 0,
102 'created_at' => current_time( 'mysql', 1 ),
103 'is_template' => $template ? 1 : 0,
104 );
105
106 if ( $blog_id ) {
107 $new_values['status'] = 'published';
108 $new_options = $values->options;
109 FrmAppHelper::unserialize_or_decode( $new_options );
110 $new_options['email_to'] = get_option( 'admin_email' );
111 $new_options['copy'] = false;
112 $new_values['options'] = $new_options;
113 } else {
114 $new_values['options'] = $values->options;
115 }
116
117 if ( is_array( $new_values['options'] ) ) {
118 $new_values['options'] = serialize( $new_values['options'] );
119 }
120
121 $query_results = $wpdb->insert( $wpdb->prefix . 'frm_forms', $new_values );
122
123 if ( $query_results ) {
124 // Clear form caching
125 self::clear_form_cache();
126
127 $form_id = $wpdb->insert_id;
128 FrmField::duplicate( $id, $form_id, $copy_keys, $blog_id );
129
130 // Update form settings after fields are created
131 do_action( 'frm_after_duplicate_form', $form_id, $new_values, array( 'old_id' => $id ) );
132
133 return $form_id;
134 }
135
136 return false;
137 }
138
139 /**
140 * @param int $form_id
141 * @param array $values
142 *
143 * @return void
144 */
145 public static function after_duplicate( $form_id, $values ) {
146 $new_opts = $values['options'];
147 FrmAppHelper::unserialize_or_decode( $new_opts );
148 $values['options'] = $new_opts;
149
150 if ( isset( $new_opts['success_msg'] ) ) {
151 $new_opts['success_msg'] = FrmFieldsHelper::switch_field_ids( $new_opts['success_msg'] );
152 }
153
154 $new_opts = apply_filters( 'frm_after_duplicate_form_values', $new_opts, $form_id );
155
156 // phpcs:ignore Universal.Operators.StrictComparisons
157 if ( $new_opts != $values['options'] ) {
158 global $wpdb;
159 $wpdb->update( $wpdb->prefix . 'frm_forms', array( 'options' => maybe_serialize( $new_opts ) ), array( 'id' => $form_id ) );
160 }
161
162 self::switch_field_ids_in_fields( $form_id );
163 }
164
165 /**
166 * Switches field ID in fields.
167 *
168 * @since 5.3
169 * @since 6.35 The description column is checked too, so a field id in a description survives a
170 * duplicate or import when the field it points at is created afterwards.
171 *
172 * @param int $form_id Form ID.
173 *
174 * @return void
175 */
176 private static function switch_field_ids_in_fields( $form_id ) {
177 global $wpdb;
178
179 // Keys of fields that you want to check to replace field ID.
180 $keys = array( 'default_value', 'description', 'field_options' );
181 $sql_cols = 'fi.id';
182
183 foreach ( $keys as $key ) {
184 $sql_cols .= ',fi.' . $key;
185 }
186
187 $fields = FrmDb::get_results(
188 "{$wpdb->prefix}frm_fields AS fi LEFT OUTER JOIN {$wpdb->prefix}frm_forms AS fr ON fi.form_id = fr.id",
189 array(
190 'or' => 1,
191 'fi.form_id' => $form_id,
192 'fr.parent_form_id' => $form_id,
193 ),
194 $sql_cols
195 );
196
197 if ( ! $fields || ! is_array( $fields ) ) {
198 return;
199 }
200
201 foreach ( $fields as $field ) {
202 self::switch_field_ids_in_field( (array) $field );
203 }
204 }
205
206 /**
207 * Switches field ID in a field.
208 *
209 * @since 5.3
210 *
211 * @param array $field Field array.
212 *
213 * @return void
214 */
215 private static function switch_field_ids_in_field( $field ) {
216 $new_values = array();
217
218 foreach ( $field as $key => $value ) {
219 if ( 'id' === $key || ! $value ) {
220 continue;
221 }
222
223 if ( ! is_string( $value ) && ! is_array( $value ) ) {
224 continue;
225 }
226
227 if ( 'field_options' === $key ) {
228 // Need to loop through field_options to prevent breaking serialized string when length changed.
229 FrmAppHelper::unserialize_or_decode( $value );
230 $new_val = FrmFieldsHelper::switch_field_ids( $value );
231 $new_val = serialize( $new_val );
232 } else {
233 $new_val = FrmFieldsHelper::switch_field_ids( $value );
234 }
235
236 if ( $new_val !== $value ) {
237 $new_values[ $key ] = $new_val;
238 }
239 }//end foreach
240
241 if ( $new_values ) {
242 FrmField::update( $field['id'], $new_values );
243 }
244 }
245
246 /**
247 * Update a form.
248 *
249 * @param int $id Form ID.
250 * @param array $values Form values to update.
251 * @param bool $create_link Whether this is being called from link creation.
252 *
253 * @return bool|int
254 */
255 public static function update( $id, $values, $create_link = false ) {
256 global $wpdb;
257
258 $values = FrmAppHelper::maybe_filter_array( $values, array( 'name', 'description' ) );
259
260 if ( ! isset( $values['status'] ) && ( $create_link || isset( $values['options'] ) || isset( $values['item_meta'] ) || isset( $values['field_options'] ) ) ) {
261 $values['status'] = 'published';
262 }
263
264 if ( isset( $values['form_key'] ) ) {
265 $values['form_key'] = FrmAppHelper::get_unique_key( $values['form_key'], $wpdb->prefix . 'frm_forms', 'form_key', $id );
266 }
267
268 $form_fields = array( 'form_key', 'name', 'description', 'status', 'parent_form_id' );
269 $new_values = self::set_update_options( array(), $values, array( 'form_id' => $id ) );
270
271 foreach ( $values as $value_key => $value ) {
272 if ( $value_key && in_array( $value_key, $form_fields, true ) ) {
273 $new_values[ $value_key ] = 'name' === $value_key ? FrmAppHelper::truncate( $value, 255, 1, '', true ) : $value;
274 }
275 }
276
277 if ( ! empty( $values['new_status'] ) ) {
278 $new_values['status'] = $values['new_status'];
279 }
280
281 if ( $new_values ) {
282 $query_results = $wpdb->update( $wpdb->prefix . 'frm_forms', $new_values, array( 'id' => $id ) );
283
284 if ( $query_results ) {
285 self::clear_form_cache();
286 }
287 } else {
288 $query_results = true;
289 }
290 unset( $new_values );
291
292 $values = self::update_fields( $id, $values );
293
294 do_action( 'frm_update_form', $id, $values );
295 do_action( 'frm_update_form_' . $id, $values );
296
297 return $query_results;
298 }
299
300 /**
301 * @param array $new_values
302 * @param array $values
303 * @param array $args
304 *
305 * @return array
306 */
307 public static function set_update_options( $new_values, $values, $args = array() ) {
308 if ( ! isset( $values['options'] ) ) {
309 return $new_values;
310 }
311
312 $options = ! empty( $values['options'] ) ? (array) $values['options'] : array();
313 FrmFormsHelper::fill_form_options( $options, $values );
314
315 $options['custom_style'] = $values['options']['custom_style'] ?? 0;
316 $options['before_html'] = $values['options']['before_html'] ?? FrmFormsHelper::get_default_html( 'before' );
317 $options['after_html'] = $values['options']['after_html'] ?? FrmFormsHelper::get_default_html( 'after' );
318 $options['submit_html'] = isset( $values['options']['submit_html'] ) && '' !== $values['options']['submit_html'] ? $values['options']['submit_html'] : FrmFormsHelper::get_default_html( 'submit' ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
319
320 /**
321 * Allows modifying form options before updating or creating.
322 *
323 * @since 5.4 Added the third param.
324 *
325 * @param array $options Form options.
326 * @param array $values Form data.
327 * @param bool $update Is form updating or creating. It's `true` if is updating.
328 */
329 $options = apply_filters( 'frm_form_options_before_update', $options, $values, true );
330 $options = self::maybe_filter_form_options( $options );
331 $new_values['options'] = serialize( $options );
332
333 return $new_values;
334 }
335
336 /**
337 * @param int $id Form ID.
338 * @param array $values Form values array.
339 *
340 * @return array
341 */
342 public static function update_fields( $id, $values ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
343
344 if ( ! isset( $values['item_meta'] ) && ! isset( $values['field_options'] ) ) {
345 return $values;
346 }
347
348 $all_fields = FrmField::get_all_for_form( $id );
349
350 if ( ! $all_fields ) {
351 return $values;
352 }
353
354 if ( ! isset( $values['item_meta'] ) ) {
355 $values['item_meta'] = array();
356 }
357
358 $field_array = array();
359 $existing_keys = array_keys( $values['item_meta'] );
360
361 foreach ( $all_fields as $fid ) {
362 // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict, SlevomatCodingStandard.Files.LineLength.LineTooLong
363 if ( ! in_array( $fid->id, $existing_keys ) && ( isset( $values['frm_fields_submitted'] ) && in_array( $fid->id, $values['frm_fields_submitted'] ) ) || isset( $values['options'] ) ) {
364 $values['item_meta'][ $fid->id ] = '';
365 }
366
367 $field_array[ $fid->id ] = $fid;
368 }
369 unset( $all_fields );
370
371 foreach ( $values['item_meta'] as $field_id => $default_value ) {
372 $field = $field_array[ $field_id ] ?? FrmField::getOne( $field_id );
373
374 if ( ! $field ) {
375 continue;
376 }
377
378 $is_settings_page = isset( $values['options'] ) || isset( $values['field_options'][ 'custom_html_' . $field_id ] );
379
380 if ( $is_settings_page ) {
381 self::get_settings_page_html( $values, $field );
382
383 if ( ! defined( 'WP_IMPORTING' ) ) {
384 continue;
385 }
386 }
387
388 // Updating the form.
389 $update_options = FrmFieldsHelper::get_default_field_options_from_field( $field );
390 // Don't check for POST html.
391 unset( $update_options['custom_html'] );
392 $update_options = apply_filters( 'frm_field_options_to_update', $update_options );
393
394 if ( ! is_array( $field->field_options ) ) {
395 $field->field_options = array();
396 }
397
398 foreach ( $update_options as $opt => $default ) {
399 $field->field_options[ $opt ] = $values['field_options'][ $opt . '_' . $field_id ] ?? $default;
400 self::sanitize_field_opt( $opt, $field->field_options[ $opt ] );
401 }
402
403 $field->field_options = apply_filters( 'frm_update_field_options', $field->field_options, $field, $values );
404
405 $new_field = array(
406 'field_options' => $field->field_options,
407 'default_value' => isset( $values[ 'default_value_' . $field_id ] ) ? FrmAppHelper::maybe_json_encode( $values[ 'default_value_' . $field_id ] ) : '',
408 );
409
410 if ( ! FrmAppHelper::allow_unfiltered_html() && isset( $values['field_options'][ 'options_' . $field_id ] ) && is_array( $values['field_options'][ 'options_' . $field_id ] ) ) { // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
411 foreach ( $values['field_options'][ 'options_' . $field_id ] as $option_key => $option ) {
412 if ( ! is_array( $option ) ) {
413 continue;
414 }
415
416 foreach ( $option as $key => $item ) {
417 $values['field_options'][ 'options_' . $field_id ][ $option_key ][ $key ] = FrmAppHelper::kses( $item, 'all' );
418 }
419 }
420 }
421
422 self::prepare_field_update_values( $field, $values, $new_field );
423 self::maybe_update_max_option( $field, $values, $new_field );
424
425 FrmField::update( $field_id, $new_field );
426
427 FrmField::delete_form_transient( $field->form_id );
428 }//end foreach
429 self::clear_form_cache();
430
431 return $values;
432 }
433
434 /**
435 * Resets the 'max' option of a field when changing paragraph field type to other field types like text, email etc.
436 *
437 * @since 6.7
438 *
439 * @param object $field
440 * @param array $values
441 * @param array $new_field
442 *
443 * @return void
444 */
445 private static function maybe_update_max_option( $field, $values, &$new_field ) {
446 if ( $field->type !== 'textarea' ||
447 empty( $values['field_options'][ 'type_' . $field->id ] ) ||
448 ! in_array( $values['field_options'][ 'type_' . $field->id ], array( 'text', 'email', 'url', 'password', 'phone' ), true ) ) {
449 return;
450 }
451
452 $new_field['field_options']['max'] = '';
453
454 /**
455 * Update posted field setting so that new 'max' option is displayed after form is saved and page reloads.
456 * FrmFieldsHelper::fill_default_field_opts populates field options by calling self::get_posted_field_setting.
457 */
458 $_POST['field_options'][ 'max_' . $field->id ] = '';
459 }
460
461 /**
462 * @param string $opt
463 * @param mixed $value
464 *
465 * @return void
466 */
467 private static function sanitize_field_opt( $opt, &$value ) {
468 if ( ! is_string( $value ) ) {
469 return;
470 }
471
472 /**
473 * Allow the option to turn off sanitization for a field. This way a custom rule can be used instead.
474 * Make sure to add custom sanitization using the frm_update_field_options filter as the data will no longer be sanitized.
475 *
476 * @since 6.0
477 *
478 * @param bool $should_sanitize
479 * @param string $opt
480 */
481 $should_sanitize = apply_filters( 'frm_should_sanitize_field_opt_string', true, $opt );
482
483 if ( ! $should_sanitize ) {
484 return;
485 }
486
487 $value = $opt === 'calc' ? self::sanitize_calc( $value ) : FrmAppHelper::kses( $value, 'all' );
488 $value = trim( $value );
489 }
490
491 /**
492 * @param string $value
493 *
494 * @return string
495 */
496 private static function sanitize_calc( $value ) {
497 if ( str_contains( $value, '<' ) ) {
498 $value = self::normalize_calc_spaces( $value );
499 }
500 // Allow <= and >=.
501 $allow = array( '<= ', ' >=' );
502 $temp = array( '< = ', ' > =' );
503 $value = str_replace( $allow, $temp, $value );
504 $value = strip_tags( $value );
505 return str_replace( $temp, $allow, $value );
506 }
507
508 /**
509 * Format a comparison like 5<10 to 5 < 10. Also works on 5< 10, 5 <10, 5<=10 variations.
510 * This is to avoid an issue with unspaced calculations being recognized as HTML that gets removed when strip_tags is called.
511 *
512 * @param string $calc
513 *
514 * @return string
515 */
516 private static function normalize_calc_spaces( $calc ) {
517 // Check for a pattern with 5 parts
518 // $1 \d|\] the first comparison digit or the end of a comparison shortcode.
519 // $2 a space (optional).
520 // $3 an equals sign (optional) that follows the < operator for <= comparisons.
521 // $4 another space (optional).
522 // $5 \d|\[ the second comparison digit or the start of a comparison shortcode.
523 return preg_replace( '/(\d|\])( ){0,1}<(=){0,1}( ){0,1}(\d|\[)/', '$1 <$3 $5', $calc );
524 }
525
526 /**
527 * Updating the settings page
528 *
529 * @param array $values Form values array.
530 * @param object $field Field object, passed by reference.
531 *
532 * @return void
533 */
534 private static function get_settings_page_html( $values, &$field ) {
535 if ( isset( $values['field_options'][ 'custom_html_' . $field->id ] ) ) {
536 $prev_opts = array();
537 $fallback_html = $field->field_options['custom_html'] ?? FrmFieldsHelper::get_default_html( $field->type );
538
539 $field->field_options['custom_html'] = $values['field_options'][ 'custom_html_' . $field->id ] ?? $fallback_html;
540 } elseif ( $field->type === 'hidden' || $field->type === 'user_id' ) {
541 $prev_opts = $field->field_options;
542 }
543
544 if ( ! isset( $prev_opts ) ) {
545 return;
546 }
547
548 $field->field_options = apply_filters( 'frm_update_form_field_options', $field->field_options, $field, $values );
549
550 // phpcs:ignore Universal.Operators.StrictComparisons
551 if ( $prev_opts != $field->field_options ) {
552 FrmField::update( $field->id, array( 'field_options' => $field->field_options ) );
553 }
554 }
555
556 /**
557 * @param object $field
558 * @param array $values
559 * @param array $new_field
560 *
561 * @return void
562 */
563 private static function prepare_field_update_values( $field, $values, &$new_field ) {
564 $field_cols = array(
565 'field_order' => 0,
566 'field_key' => '',
567 'required' => false,
568 'type' => '',
569 'description' => '',
570 'options' => '',
571 'name' => '',
572 );
573
574 foreach ( $field_cols as $col => $default ) {
575 $default = $default === '' ? $field->{$col} : $default;
576 $new_field[ $col ] = $values['field_options'][ $col . '_' . $field->id ] ?? $default;
577 }
578
579 if ( $field->type === 'submit' && isset( $new_field['field_order'] ) && (int) $new_field['field_order'] === FrmSubmitHelper::DEFAULT_ORDER ) {
580 $new_field['field_order'] = $field->field_order;
581 }
582
583 // Don't save the template option.
584 if ( is_array( $new_field['options'] ) && isset( $new_field['options']['000'] ) ) {
585 unset( $new_field['options']['000'] );
586 }
587 }
588
589 /**
590 * Get a list of all form settings that should be translated
591 * on a multilingual site.
592 *
593 * @since 3.06.01
594 *
595 * @param object $form The form object.
596 *
597 * @return array
598 */
599 public static function translatable_strings( $form ) {
600 $strings = array(
601 'name',
602 'description',
603 'submit_value',
604 'submit_msg',
605 'success_msg',
606 'invalid_msg',
607 'failed_msg',
608 'login_msg',
609 'admin_permission',
610 );
611
612 return apply_filters( 'frm_form_strings', $strings, $form );
613 }
614
615 /**
616 * @param array|int $id
617 * @param string $status
618 *
619 * @return bool|int
620 */
621 public static function set_status( $id, $status ) {
622 if ( 'trash' === $status ) {
623 return self::trash( $id );
624 }
625
626 $statuses = array( 'published', 'draft', 'trash' );
627
628 if ( ! in_array( $status, $statuses, true ) ) {
629 return false;
630 }
631
632 global $wpdb;
633
634 if ( is_array( $id ) ) {
635 $where = array(
636 'id' => $id,
637 'parent_form_id' => $id,
638 'or' => 1,
639 );
640 FrmDb::get_where_clause_and_values( $where );
641 array_unshift( $where['values'], $status );
642
643 $query_results = $wpdb->query( $wpdb->prepare( 'UPDATE ' . $wpdb->prefix . 'frm_forms SET status = %s ' . $where['where'], $where['values'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, SlevomatCodingStandard.Files.LineLength.LineTooLong
644 } else {
645 $query_results = $wpdb->update( $wpdb->prefix . 'frm_forms', array( 'status' => $status ), array( 'id' => $id ) );
646 $wpdb->update( $wpdb->prefix . 'frm_forms', array( 'status' => $status ), array( 'parent_form_id' => $id ) );
647 }
648
649 if ( $query_results ) {
650 self::clear_form_cache();
651 }
652
653 return $query_results;
654 }
655
656 /**
657 * @param int|string $id Form ID.
658 *
659 * @return bool|int
660 */
661 public static function trash( $id ) {
662 if ( ! EMPTY_TRASH_DAYS ) {
663 return self::destroy( $id );
664 }
665
666 $form = self::getOne( $id );
667
668 if ( ! $form ) {
669 return false;
670 }
671
672 if ( $form->status === 'trash' ) {
673 return false;
674 }
675
676 $options = $form->options;
677 $options['trash_time'] = time();
678
679 global $wpdb;
680 $query_results = $wpdb->update(
681 $wpdb->prefix . 'frm_forms',
682 array(
683 'status' => 'trash',
684 'options' => serialize( $options ),
685 ),
686 array(
687 'id' => $id,
688 )
689 );
690
691 $wpdb->update(
692 $wpdb->prefix . 'frm_forms',
693 array(
694 'status' => 'trash',
695 'options' => serialize( $options ),
696 ),
697 array(
698 'parent_form_id' => $id,
699 )
700 );
701
702 if ( $query_results ) {
703 self::clear_form_cache();
704 }
705
706 return $query_results;
707 }
708
709 /**
710 * @param int|string $id Form ID.
711 *
712 * @return bool|int
713 */
714 public static function destroy( $id ) {
715 global $wpdb;
716
717 $form = self::getOne( $id );
718
719 if ( ! $form ) {
720 return false;
721 }
722
723 $id = $form->id;
724
725 // Disconnect the entries from this form
726 $entries = FrmDb::get_col( 'frm_items', array( 'form_id' => $id ) );
727
728 foreach ( $entries as $entry_id ) {
729 FrmEntry::destroy( $entry_id );
730 unset( $entry_id );
731 }
732
733 // Disconnect the fields from this form
734 $wpdb->query( $wpdb->prepare( 'DELETE fi FROM ' . $wpdb->prefix . 'frm_fields AS fi LEFT JOIN ' . $wpdb->prefix . 'frm_forms fr ON (fi.form_id = fr.id) WHERE fi.form_id=%d OR parent_form_id=%d', $id, $id ) ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
735
736 $query_results = $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_forms WHERE id=%d OR parent_form_id=%d', $id, $id ) );
737
738 if ( ! $query_results ) {
739 return $query_results;
740 }
741
742 // Delete all form actions linked to this form
743 /**
744 * @var FrmFormAction
745 */
746 $action_control = FrmFormActionsController::get_form_actions( 'email' );
747 $action_control->destroy( $id, 'all' );
748
749 // Clear form caching
750 self::clear_form_cache();
751
752 do_action( 'frm_destroy_form', $id );
753 do_action( 'frm_destroy_form_' . $id );
754
755 return $query_results;
756 }
757
758 /**
759 * Delete trashed forms based on how long they have been trashed
760 *
761 * @param int|string $delete_timestamp Timestamp cutoff for deletion.
762 *
763 * @return int The number of forms deleted
764 */
765 public static function scheduled_delete( $delete_timestamp = '' ) {
766 $trash_forms = FrmDb::get_results( 'frm_forms', array( 'status' => 'trash' ), 'id, parent_form_id, options' );
767
768 if ( ! $trash_forms ) {
769 return 0;
770 }
771
772 if ( ! $delete_timestamp ) {
773 $delete_timestamp = time() - ( DAY_IN_SECONDS * EMPTY_TRASH_DAYS );
774 }
775
776 $count = 0;
777
778 foreach ( $trash_forms as $form ) {
779 FrmAppHelper::unserialize_or_decode( $form->options );
780
781 if ( ! isset( $form->options['trash_time'] ) || $form->options['trash_time'] < $delete_timestamp ) {
782 self::destroy( $form->id );
783
784 if ( empty( $form->parent_form_id ) ) {
785 ++$count;
786 }
787 }
788
789 unset( $form );
790 }
791
792 return $count;
793 }
794
795 /**
796 * @param int|string $id Form ID or key.
797 *
798 * @return string form name
799 */
800 public static function getName( $id ) {
801 $form = FrmDb::check_cache( $id, 'frm_form' );
802
803 if ( $form ) {
804 return stripslashes( $form->name );
805 }
806
807 $query_key = is_numeric( $id ) ? 'id' : 'form_key';
808 $r = FrmDb::get_var( 'frm_forms', array( $query_key => $id ), 'name' );
809
810 // An empty form name can result in a null value.
811 return is_null( $r ) ? '' : stripslashes( $r );
812 }
813
814 /**
815 * @since 3.0
816 *
817 * @param string $key
818 *
819 * @return int form id
820 */
821 public static function get_id_by_key( $key ) {
822 return (int) FrmDb::get_var( 'frm_forms', array( 'form_key' => sanitize_title( $key ) ) );
823 }
824
825 /**
826 * @since 3.0
827 *
828 * @param int|string $id
829 *
830 * @return string form key
831 */
832 public static function get_key_by_id( $id ) {
833 $id = (int) $id;
834 $cache = FrmDb::check_cache( $id, 'frm_form' );
835
836 if ( $cache ) {
837 return $cache->form_key;
838 }
839
840 return FrmDb::get_var( 'frm_forms', array( 'id' => $id ), 'form_key' );
841 }
842
843 /**
844 * If $form is numeric, get the form object
845 *
846 * @since 2.0.9
847 *
848 * @param array|int|object $form
849 *
850 * @return void
851 */
852 public static function maybe_get_form( &$form ) {
853 if ( ! is_object( $form ) && ! is_array( $form ) && $form ) {
854 $form = self::getOne( $form );
855 }
856 }
857
858 /**
859 * @param int|string $id
860 * @param false|int $blog_id
861 *
862 * @return stdClass|null
863 */
864 public static function getOne( $id, $blog_id = false ) {
865 global $wpdb;
866
867 if ( $blog_id && is_multisite() ) {
868 $prefix = $wpdb->get_blog_prefix( $blog_id );
869 $table_name = $prefix . 'frm_forms';
870 } else {
871 $table_name = $wpdb->prefix . 'frm_forms';
872 $cache = wp_cache_get( $id, 'frm_form' );
873
874 if ( $cache ) {
875 if ( isset( $cache->options ) ) {
876 FrmAppHelper::unserialize_or_decode( $cache->options );
877 }
878
879 return self::prepare_form_row_data( $cache );
880 }
881 }
882
883 $where = is_numeric( $id ) ? array( 'id' => $id ) : array( 'form_key' => $id );
884 $results = FrmDb::get_row( $table_name, $where );
885
886 if ( isset( $results->options ) ) {
887 FrmDb::set_cache( $results->id, $results, 'frm_form' );
888 FrmAppHelper::unserialize_or_decode( $results->options );
889 }
890
891 return self::prepare_form_row_data( $results );
892 }
893
894 /**
895 * Make sure that if $row is an object, that $row->options is an array and not a string.
896 *
897 * @since 6.8.3
898 *
899 * @param stdClass|null $row The database row for a target form.
900 *
901 * @return stdClass|null
902 */
903 private static function prepare_form_row_data( $row ) {
904 $row = wp_unslash( $row );
905
906 if ( ! is_object( $row ) ) {
907 return $row;
908 }
909
910 if ( ! is_array( $row->options ) ) {
911 $row->options = FrmFormsHelper::get_default_opts();
912 }
913
914 /**
915 * @since 4.03.02
916 *
917 * @param stdClass $row
918 */
919 return apply_filters( 'frm_form_object', $row );
920 }
921
922 /**
923 * @param array|string $where Where conditions array or raw WHERE string.
924 * @param string $order_by Order by clause.
925 * @param int|string $limit Limit clause or number.
926 *
927 * @return array|object Array of objects. If $limit is 1, a single object is returned.
928 */
929 public static function getAll( $where = array(), $order_by = '', $limit = '' ) {
930 if ( is_array( $where ) && $where ) {
931 if ( ! empty( $where['is_template'] ) && ! isset( $where['status'] ) && ! isset( $where['status !'] ) ) {
932 // Don't get trashed templates
933 $where['status'] = array( null, '', 'published' );
934 }
935
936 $results = FrmDb::get_results( 'frm_forms', $where, '*', compact( 'order_by', 'limit' ) );
937 } else {
938 global $wpdb;
939
940 // The query has already been prepared if this is not an array.
941 $query = 'SELECT * FROM ' . $wpdb->prefix . 'frm_forms' . FrmDb::prepend_and_or_where( ' WHERE ', $where ) . FrmDb::esc_order( $order_by ) . FrmDb::esc_limit( $limit ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
942 $results = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
943 }
944
945 if ( $results ) {
946 foreach ( $results as $result ) {
947 FrmDb::set_cache( $result->id, $result, 'frm_form' );
948 FrmAppHelper::unserialize_or_decode( $result->options );
949 }
950 }
951
952 if ( $limit === ' LIMIT 1' || (int) $limit === 1 ) {
953 // Return the first form object if we are only getting one form
954 $results = reset( $results );
955 }
956
957 return wp_unslash( $results );
958 }
959
960 /**
961 * Get all published forms
962 *
963 * @since 2.0
964 *
965 * @param array $query
966 * @param int $limit
967 * @param string $inc_children
968 *
969 * @return array|object Array of forms. A single form object is returned if $limit is set to 1.
970 */
971 public static function get_published_forms( $query = array(), $limit = 999, $inc_children = 'exclude' ) {
972 $query['is_template'] = 0;
973 $query['status'] = array( null, '', 'published' );
974
975 if ( $inc_children === 'exclude' ) {
976 $query['parent_form_id'] = array( null, 0 );
977 }
978
979 return self::getAll( $query, 'name', $limit );
980 }
981
982 /**
983 * @return object count of forms
984 */
985 public static function get_count() {
986 $cache_key = 'frm_form_counts';
987 $counts = wp_cache_get( $cache_key, 'frm_form' );
988
989 if ( false !== $counts ) {
990 return $counts;
991 }
992
993 $results = FrmDb::get_results(
994 'frm_forms',
995 array(
996 'or' => 1,
997 'parent_form_id' => null,
998 'parent_form_id <' => 0,
999 ),
1000 'status, is_template'
1001 );
1002
1003 $statuses = array( 'published', 'draft', 'template', 'trash' );
1004 $counts = array_fill_keys( $statuses, 0 );
1005
1006 foreach ( $results as $row ) {
1007 if ( 'trash' === $row->status ) {
1008 ++$counts['trash'];
1009 } elseif ( $row->is_template ) {
1010 ++$counts['template'];
1011 } else {
1012 ++$counts['published'];
1013 }
1014
1015 if ( 'draft' === $row->status ) {
1016 ++$counts['draft'];
1017 }
1018
1019 unset( $row );
1020 }
1021
1022 $counts = (object) $counts;
1023 FrmDb::set_cache( $cache_key, $counts, 'frm_form' );
1024
1025 return $counts;
1026 }
1027
1028 /**
1029 * Clear form caching
1030 * Called when a form is created, updated, duplicated, or deleted
1031 * or when the form status is changed
1032 *
1033 * @since 2.0.4
1034 *
1035 * @return void
1036 */
1037 public static function clear_form_cache() {
1038 FrmDb::cache_delete_group( 'frm_form' );
1039 }
1040
1041 /**
1042 * @param array $values Form values to validate.
1043 *
1044 * @return array of errors
1045 */
1046 public static function validate( $values ) {
1047 $errors = array();
1048 return apply_filters( 'frm_validate_form', $errors, $values );
1049 }
1050
1051 /**
1052 * @param object|null $form
1053 *
1054 * @return array
1055 */
1056 public static function get_params( $form = null ) {
1057 global $frm_vars;
1058
1059 if ( $form ) {
1060 self::maybe_get_form( $form );
1061 } else {
1062 $form = self::getAll( array(), 'name', 1 );
1063 }
1064
1065 if ( isset( $frm_vars['form_params'] ) && is_array( $frm_vars['form_params'] ) && isset( $frm_vars['form_params'][ $form->id ] ) ) {
1066 return $frm_vars['form_params'][ $form->id ];
1067 }
1068
1069 $action_var = isset( $_REQUEST['frm_action'] ) ? 'frm_action' : 'action'; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1070 $action = apply_filters( 'frm_show_new_entry_page', FrmAppHelper::get_param( $action_var, 'new', 'get', 'sanitize_title' ), $form );
1071
1072 $default_values = array(
1073 'id' => '',
1074 'form_name' => '',
1075 'paged' => 1,
1076 'form' => $form->id,
1077 'form_id' => $form->id,
1078 'field_id' => '',
1079 'search' => '',
1080 'sort' => '',
1081 'sdir' => '',
1082 'action' => $action,
1083 );
1084
1085 $values = array();
1086 $values['posted_form_id'] = FrmAppHelper::get_param( 'form_id', '', 'get', 'absint' );
1087
1088 if ( ! $values['posted_form_id'] ) {
1089 $values['posted_form_id'] = FrmAppHelper::get_param( 'form', '', 'get', 'absint' );
1090 }
1091
1092 // phpcs:ignore Universal.Operators.StrictComparisons
1093 if ( $form->id == $values['posted_form_id'] ) {
1094 // If there are two forms on the same page, make sure not to submit both.
1095 foreach ( $default_values as $var => $default ) {
1096 if ( $var === 'action' ) {
1097 $values[ $var ] = FrmAppHelper::get_param( $action_var, $default, 'get', 'sanitize_title' );
1098 } else {
1099 $values[ $var ] = FrmAppHelper::get_param( $var, $default, 'get', 'sanitize_text_field' );
1100 }
1101 unset( $var, $default );
1102 }
1103 } else {
1104 foreach ( $default_values as $var => $default ) {
1105 $values[ $var ] = $default;
1106 unset( $var, $default );
1107 }
1108 }
1109
1110 if ( in_array( $values['action'], array( 'create', 'update' ), true ) &&
1111 ( ! $_POST || ( ! isset( $_POST['action'] ) && ! isset( $_POST['frm_action'] ) ) ) // phpcs:ignore WordPress.Security.NonceVerification.Missing
1112 ) {
1113 $values['action'] = 'new';
1114 }
1115
1116 return $values;
1117 }
1118
1119 /**
1120 * @return array
1121 */
1122 public static function list_page_params() {
1123 $values = array();
1124 $defaults = array(
1125 'template' => 0,
1126 'id' => '',
1127 'paged' => 1,
1128 'form' => '',
1129 'search' => '',
1130 'sort' => '',
1131 'sdir' => '',
1132 );
1133
1134 foreach ( $defaults as $var => $default ) {
1135 $values[ $var ] = FrmAppHelper::get_param( $var, $default, 'get', 'sanitize_text_field' );
1136 }
1137
1138 return $values;
1139 }
1140
1141 /**
1142 * @param int|object|string|null $form
1143 *
1144 * @return array
1145 */
1146 public static function get_admin_params( $form = null ) {
1147 $form_id = $form;
1148
1149 if ( $form === null ) {
1150 $form_id = self::get_current_form_id();
1151 } elseif ( $form && is_object( $form ) ) {
1152 $form_id = $form->id;
1153 }
1154
1155 $values = array();
1156 $defaults = array(
1157 'id' => '',
1158 'form_name' => '',
1159 'paged' => 1,
1160 'form' => $form_id,
1161 'field_id' => '',
1162 'search' => '',
1163 'sort' => '',
1164 'sdir' => '',
1165 'fid' => '',
1166 'keep_post' => '',
1167 );
1168
1169 foreach ( $defaults as $var => $default ) {
1170 $values[ $var ] = FrmAppHelper::get_param( $var, $default, 'get', 'sanitize_text_field' );
1171 }
1172
1173 return $values;
1174 }
1175
1176 /**
1177 * @param string $default_form
1178 *
1179 * @return int|string
1180 */
1181 public static function get_current_form_id( $default_form = 'none' ) {
1182 $form = 'first' === $default_form ? self::get_current_form() : self::maybe_get_current_form();
1183 return $form ? $form->id : 0;
1184 }
1185
1186 /**
1187 * @param int|string $form_id
1188 *
1189 * @return false|int|object|string
1190 */
1191 public static function maybe_get_current_form( $form_id = 0 ) {
1192 global $frm_vars;
1193
1194 if ( ! empty( $frm_vars['current_form'] ) && ( ! $form_id || (int) $form_id === (int) $frm_vars['current_form']->id ) ) {
1195 return $frm_vars['current_form'];
1196 }
1197
1198 $form_id = FrmAppHelper::get_param( 'form', $form_id, 'get', 'absint' );
1199
1200 return $form_id ? self::set_current_form( $form_id ) : $form_id;
1201 }
1202
1203 /**
1204 * @param int $form_id
1205 *
1206 * @return false|object
1207 */
1208 public static function get_current_form( $form_id = 0 ) {
1209 $form = self::maybe_get_current_form( $form_id );
1210 return is_numeric( $form ) ? self::set_current_form( $form ) : $form;
1211 }
1212
1213 /**
1214 * @param int $form_id
1215 *
1216 * @return false|object
1217 */
1218 public static function set_current_form( $form_id ) {
1219 global $frm_vars;
1220
1221 $query = array();
1222
1223 if ( $form_id ) {
1224 $query['id'] = $form_id;
1225 }
1226
1227 $frm_vars['current_form'] = self::get_published_forms( $query, 1 );
1228
1229 return $frm_vars['current_form'];
1230 }
1231
1232 /**
1233 * @param object $form
1234 * @param int|string $this_load
1235 * @param bool $global_load
1236 *
1237 * @return bool
1238 */
1239 public static function is_form_loaded( $form, $this_load, $global_load ) {
1240 global $frm_vars;
1241 $small_form = new stdClass();
1242
1243 foreach ( array( 'id', 'form_key', 'name' ) as $var ) {
1244 $small_form->{$var} = $form->{$var};
1245 unset( $var );
1246 }
1247
1248 $frm_vars['forms_loaded'][] = $small_form;
1249
1250 if ( $this_load && ! $global_load ) {
1251 $global_load = true;
1252 $frm_vars['load_css'] = true;
1253 }
1254
1255 return empty( $frm_vars['css_loaded'] ) && $global_load;
1256 }
1257
1258 /**
1259 * @since 4.06.03
1260 *
1261 * @param object $form
1262 *
1263 * @return bool
1264 */
1265 public static function is_visible_to_user( $form ) {
1266 if ( $form->logged_in && isset( $form->options['logged_in_role'] ) ) {
1267 $visible = FrmAppHelper::user_has_permission( $form->options['logged_in_role'] );
1268 } else {
1269 $visible = true;
1270 }
1271
1272 /**
1273 * @since 6.25
1274 *
1275 * @param bool $visible
1276 * @param object $form
1277 */
1278 return (bool) apply_filters( 'frm_form_is_visible', $visible, $form );
1279 }
1280
1281 /**
1282 * @param object $form
1283 *
1284 * @return bool
1285 */
1286 public static function show_submit( $form ) {
1287 $show = ! $form->is_template && $form->status === 'published' && ! FrmAppHelper::is_admin();
1288 return apply_filters( 'frm_show_submit_button', $show, $form );
1289 }
1290
1291 /**
1292 * @since 2.3
1293 *
1294 * @param array $atts Attributes including form, option, and default.
1295 *
1296 * @return mixed
1297 */
1298 public static function get_option( $atts ) {
1299 $form = $atts['form'];
1300 $default = $atts['default'] ?? '';
1301
1302 return $form->options[ $atts['option'] ] ?? $default;
1303 }
1304
1305 /**
1306 * Get the link to edit this form.
1307 *
1308 * @since 4.0
1309 *
1310 * @param int $form_id The id of the form.
1311 *
1312 * @return string
1313 */
1314 public static function get_edit_link( $form_id ) {
1315 return admin_url( 'admin.php?page=formidable&frm_action=edit&id=' . $form_id );
1316 }
1317
1318 /**
1319 * Check if the "Submit this form with AJAX" setting is toggled on.
1320 *
1321 * @since 6.2
1322 *
1323 * @param stdClass $form
1324 *
1325 * @return bool
1326 */
1327 public static function is_ajax_on( $form ) {
1328 return ! empty( $form->options['ajax_submit'] );
1329 }
1330
1331 /**
1332 * Get the latest form available.
1333 *
1334 * @since 6.8
1335 *
1336 * @return object
1337 */
1338 public static function get_latest_form() {
1339 $args = array(
1340 array(
1341 'or' => 1,
1342 'parent_form_id' => null,
1343 'parent_form_id <' => 1,
1344 ),
1345 'is_template' => 0,
1346 'status !' => 'trash',
1347 );
1348
1349 return self::getAll( $args, 'created_at desc', 1 );
1350 }
1351
1352 /**
1353 * Count and return total forms.
1354 *
1355 * @since 6.8
1356 *
1357 * @return int
1358 */
1359 public static function get_forms_count() {
1360 $args = array(
1361 array(
1362 'or' => 1,
1363 'parent_form_id' => null,
1364 'parent_form_id <' => 1,
1365 ),
1366 'is_template' => 0,
1367 'status !' => 'trash',
1368 );
1369
1370 return FrmDb::get_count( 'frm_forms', $args );
1371 }
1372
1373 /**
1374 * @deprecated 2.03.05 This is still referenced in a few add ons (API, locations).
1375 *
1376 * @codeCoverageIgnore
1377 *
1378 * @param string $key
1379 *
1380 * @return int form id
1381 */
1382 public static function getIdByKey( $key ) {
1383 _deprecated_function( __FUNCTION__, '2.03.05', 'FrmForm::get_id_by_key' );
1384 return self::get_id_by_key( $key );
1385 }
1386
1387 /**
1388 * @deprecated 2.03.05 This is still referenced in the API add on as of v1.13.
1389 *
1390 * @codeCoverageIgnore
1391 *
1392 * @param int|string $id
1393 *
1394 * @return string
1395 */
1396 public static function getKeyById( $id ) {
1397 _deprecated_function( __FUNCTION__, '2.03.05', 'FrmForm::get_key_by_id' );
1398 return self::get_key_by_id( $id );
1399 }
1400 }
1401