PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.9
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.9
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 / FrmField.php

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

1,309 lines 39.0 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 FrmField {
7
8 public static $use_cache = true;
9 public static $transient_size = 200;
10
11 public static function field_selection() {
12 $fields = array(
13 'text' => array(
14 'name' => __( 'Text', 'formidable' ),
15 'icon' => 'frm_icon_font frm_text2_icon',
16 ),
17 'textarea' => array(
18 'name' => __( 'Paragraph', 'formidable' ),
19 'icon' => 'frm_icon_font frm_paragraph_icon',
20 ),
21 'checkbox' => array(
22 'name' => __( 'Checkboxes', 'formidable' ),
23 'icon' => 'frm_icon_font frm_check_square_icon',
24 ),
25 'radio' => array(
26 'name' => __( 'Radio Buttons', 'formidable' ),
27 'icon' => 'frm_icon_font frm_radio_checked_icon',
28 ),
29 'select' => array(
30 'name' => __( 'Dropdown', 'formidable' ),
31 'icon' => 'frm_icon_font frm_caret_square_down_icon',
32 ),
33 'email' => array(
34 'name' => __( 'Email', 'formidable' ),
35 'icon' => 'frm_icon_font frm_email_icon',
36 ),
37 'url' => array(
38 'name' => __( 'Website/URL', 'formidable' ),
39 'icon' => 'frm_icon_font frm_link_icon',
40 ),
41 'number' => array(
42 'name' => __( 'Number', 'formidable' ),
43 'icon' => 'frm_icon_font frm_hashtag_icon',
44 ),
45 'name' => array(
46 'name' => __( 'Name', 'formidable' ),
47 'icon' => 'frm_icon_font frm_user_name_icon',
48 ),
49 'phone' => array(
50 'name' => __( 'Phone', 'formidable' ),
51 'icon' => 'frm_icon_font frm_phone_icon',
52 ),
53 'html' => array(
54 'name' => __( 'HTML', 'formidable' ),
55 'icon' => 'frm_icon_font frm_code_icon',
56 ),
57 'hidden' => array(
58 'name' => __( 'Hidden', 'formidable' ),
59 'icon' => 'frm_icon_font frm_eye_slash_icon',
60 ),
61 'user_id' => array(
62 'name' => __( 'User ID', 'formidable' ),
63 'icon' => 'frm_icon_font frm_user_icon',
64 ),
65 'captcha' => array(
66 'name' => self::get_captcha_field_name(),
67 'icon' => 'frm_icon_font frm_shield_check_icon',
68 ),
69 'credit_card' => array(
70 'name' => __( 'Payment', 'formidable' ),
71 'icon' => 'frm_icon_font frm_credit_card_icon',
72 ),
73 FrmSubmitHelper::FIELD_TYPE => array(
74 'name' => __( 'Submit', 'formidable' ),
75 'hide' => true,
76 ),
77 );
78
79 /**
80 * @param array $fields
81 */
82 return apply_filters( 'frm_available_fields', $fields );
83 }
84
85 /**
86 * Get the name of the Captcha field based on the global Captcha setting.
87 *
88 * @return string
89 */
90 private static function get_captcha_field_name() {
91 return 'Captcha';
92 }
93
94 public static function pro_field_selection() {
95 $images_url = FrmAppHelper::plugin_url() . '/images/';
96 $fields = array(
97 'file' => array(
98 'name' => __( 'File Upload', 'formidable' ),
99 'icon' => 'frm_icon_font frm_upload_icon',
100 'message' => 'Add file uploads to save time and cut down on back-and-forth. Upgrade to Pro to get Upload fields and more.',
101 ),
102 'ranking' => array(),
103 'rte' => array(
104 'name' => __( 'Rich Text', 'formidable' ),
105 'icon' => 'frm_icon_font frm_align_right_icon',
106 ),
107 'date' => array(
108 'name' => __( 'Date', 'formidable' ),
109 'icon' => 'frm_icon_font frm_calendar_icon',
110 ),
111 'time' => array(
112 'name' => __( 'Time', 'formidable' ),
113 'icon' => 'frm_icon_font frm_clock_icon',
114 ),
115 'scale' => array(
116 'name' => __( 'Scale', 'formidable' ),
117 'icon' => 'frm_icon_font frm_linear_scale_icon',
118 'message' => 'Add a set of radio buttons with whatever range you choose. <img src="' . esc_attr( $images_url ) . 'scale_field.png" alt="Scale Field" />',
119 ),
120 'star' => array(
121 'name' => __( 'Star Rating', 'formidable' ),
122 'icon' => 'frm_icon_font frm_star_icon',
123 ),
124 'range' => array(
125 'name' => __( 'Slider', 'formidable' ),
126 'icon' => 'frm_icon_font frm_code_commit_icon',
127 ),
128 'toggle' => array(
129 'name' => __( 'Toggle', 'formidable' ),
130 'icon' => 'frm_icon_font frm_toggle_on_icon',
131 ),
132 'data' => array(
133 'name' => __( 'Dynamic', 'formidable' ),
134 'icon' => 'frm_icon_font frm_sitemap_icon',
135 'message' => 'Create relationships between multiple forms. You can link a member to a team, a rating to a product, a comment to a submission, and much more.',
136 ),
137 'lookup' => array(
138 'name' => __( 'Lookup', 'formidable' ),
139 'icon' => 'frm_icon_font frm_search_icon',
140 'message' => 'Filter the options in the next field and automatically add values to other fields. Upgrade to Pro to get Lookup fields and more. <img src="' . esc_attr( $images_url ) . 'look-up_year-make-model.gif" alt="cascading lookup fields" />',
141 ),
142 'divider|repeat' => array(
143 'name' => __( 'Repeater', 'formidable' ),
144 'icon' => 'frm_icon_font frm_repeater_icon',
145 'message' => 'Allow your visitors to add new sets of fields while filling out forms. Increase conversions while saving building time and server resources. <img src="' . esc_attr( $images_url ) . 'repeatable-section_frontend.gif" alt="Dynamically Add Form Fields with repeatable sections" />',
146 ),
147 'end_divider' => array(
148 'name' => __( 'Section Buttons', 'formidable' ),
149 'switch_from' => 'divider',
150 ),
151 'divider' => array(
152 'name' => __( 'Section', 'formidable' ),
153 'icon' => 'frm_icon_font frm_header_icon',
154 ),
155 'break' => array(
156 'name' => __( 'Page Break', 'formidable' ),
157 'icon' => 'frm_icon_font frm_page_break_icon',
158 'message' => 'Get multi-paged forms with progress bars. Did you know you can upgrade to PRO to unlock multi-step forms with more awesome features?',
159 ),
160 'form' => array(
161 'name' => __( 'Embed Form', 'formidable' ),
162 'icon' => 'frm_icon_font frm_file_text_icon',
163 ),
164 'likert' => array(
165 'name' => __( 'Likert Scale', 'formidable' ),
166 'icon' => 'frm_icon_font frm_likert_scale frm_show_upgrade',
167 'addon' => 'surveys',
168 ),
169 'nps' => array(
170 'name' => __( 'NPS', 'formidable' ),
171 'icon' => 'frm_icon_font frm_nps frm_show_upgrade',
172 'addon' => 'surveys',
173 ),
174 'password' => array(
175 'name' => __( 'Password', 'formidable' ),
176 'icon' => 'frm_icon_font frm_lock_open_icon',
177 ),
178 'tag' => array(
179 'name' => __( 'Tags', 'formidable' ),
180 'icon' => 'frm_icon_font frm_price_tags_icon',
181 ),
182 // This is no longer a Pro field, but without this here, Pro triggers "undefined index" notices.
183 // Right now it leaves a gap. Maybe we can skip anything without a name or something.
184 'credit_card' => array(
185 'name' => '',
186 'icon' => '',
187 ),
188 'address' => array(
189 'name' => __( 'Address', 'formidable' ),
190 'icon' => 'frm_icon_font frm_location_icon',
191 ),
192 'summary' => array(
193 'name' => __( 'Summary', 'formidable' ),
194 'icon' => 'frm_icon_font frm_file_text_icon',
195 'message' => 'Allow visitors to review their responses before a form is submitted. Upgrade to Pro to get Summary fields and more.',
196 ),
197 'signature' => array(
198 'name' => __( 'Signature', 'formidable' ),
199 'icon' => 'frm_icon_font frm_signature_icon frm_show_upgrade',
200 'addon' => 'signature',
201 ),
202 'ai' => array(
203 'name' => __( 'AI', 'formidable' ),
204 'icon' => 'frm_icon_font frm_eye_icon frm_show_upgrade',
205 'addon' => 'ai',
206 'message' => 'Streamline workflows and reclaim valuable time with the power of AI. You can effortlessly respond to your visitors in real-time with ChatGPT as your automated assistant. Upgrade to Pro and unlock AI-powered fields.',
207 ),
208 'ssa-appointment' => array(
209 'name' => __( 'Appointment', 'formidable' ),
210 'icon' => 'frm_icon_font frm_calendar_icon frm_show_upgrade',
211 'require' => 'Simply Schedule Appointments',
212 'message' => 'Appointment fields are an integration with <a href="https://simplyscheduleappointments.com/meet/formidable/">Simply Schedule Appointments</a>. Get started now to schedule appointments directly from your forms.
213 <img src="' . esc_attr( $images_url ) . 'appointments.png" alt="Scheduling" />',
214 'link' => 'https://simplyscheduleappointments.com/meet/formidable/',
215 ),
216 'product' => array(
217 'name' => __( 'Product', 'formidable' ),
218 'icon' => 'frm_icon_font frm_product_icon',
219 'section' => 'pricing',
220 ),
221 'quantity' => array(
222 'name' => __( 'Quantity', 'formidable' ),
223 'icon' => 'frm_icon_font frm_quantity_icon',
224 'section' => 'pricing',
225 ),
226 'total' => array(
227 'name' => __( 'Total', 'formidable' ),
228 'icon' => 'frm_icon_font frm_total_icon',
229 'section' => 'pricing',
230 ),
231 );
232
233 if ( self::include_ranking_fields() ) {
234 $fields['ranking'] = array(
235 'name' => __( 'Ranking', 'formidable' ),
236 'icon' => 'frm_icon_font frm_chart_bar_icon frm_show_upgrade',
237 'message' => 'Now you can effortlessly gather insights, preferences, and opinions by allowing users to rank options.',
238 'upsell_image' => $images_url . 'ranking-field.svg',
239 'addon' => 'surveys',
240 'is_new' => self::field_is_new( 'ranking' ),
241 );
242 } else {
243 unset( $fields['ranking'] );
244 }
245
246 if ( ! FrmAppHelper::show_new_feature( 'ai' ) ) {
247 unset( $fields['ai'] );
248 }
249
250 // Since the signature field may be in a different section, don't show it twice.
251 $lite_fields = self::field_selection();
252 if ( isset( $lite_fields['signature'] ) ) {
253 unset( $fields['signature'] );
254 }
255
256 return apply_filters( 'frm_pro_available_fields', $fields );
257 }
258
259 /**
260 * Check if we should show ranking fields in the builder.
261 * This is based on the active version coming from our API data.
262 * If Surveys v1.1 is not released yet, we don't want to display ranking fields yet.
263 *
264 * @since 6.8.3
265 *
266 * @return bool
267 */
268 private static function include_ranking_fields() {
269 if ( class_exists( 'FrmSurveys\models\fields\Ranking' ) ) {
270 // Always return true if Ranking fields exist.
271 return true;
272 }
273
274 $plugin = 'formidable-surveys/formidable-surveys.php';
275 $expected_version = '1.1';
276
277 return self::installed_plugin_meets_version( $plugin, $expected_version ) || self::api_meets_version( $plugin, $expected_version );
278 }
279
280 /**
281 * @since 6.8.3
282 *
283 * @param string $plugin
284 * @param string $expected_version
285 * @return bool
286 */
287 private static function installed_plugin_meets_version( $plugin, $expected_version ) {
288 $installed_version = self::get_installed_version( $plugin );
289 return $installed_version && version_compare( $installed_version, $expected_version, '>=' );
290 }
291
292 /**
293 * @since 6.8.3
294 *
295 * @param string $plugin
296 * @return string|false String version. False if the plugin is not installed.
297 */
298 private static function get_installed_version( $plugin ) {
299 if ( ! function_exists( 'get_plugins' ) ) {
300 require_once ABSPATH . 'wp-admin/includes/plugin.php';
301 }
302 $plugins = get_plugins();
303 if ( isset( $plugins[ $plugin ] ) && ! empty( $plugins[ $plugin ]['Version'] ) ) {
304 return $plugins[ $plugin ]['Version'];
305 }
306 return false;
307 }
308
309 /**
310 * @since 6.8.3
311 *
312 * @param string $plugin
313 * @param string $expected_version
314 * @return bool
315 */
316 private static function api_meets_version( $plugin, $expected_version ) {
317 $api = new FrmFormApi();
318 $addons = $api->get_api_info();
319 $matches = wp_list_filter( $addons, array( 'plugin' => $plugin ) );
320 if ( ! $matches ) {
321 return false;
322 }
323
324 $match = reset( $matches );
325 if ( empty( $match['new_version'] ) ) {
326 return false;
327 }
328
329 $api_version = $match['new_version'];
330 return version_compare( $api_version, $expected_version, '>=' );
331 }
332
333 /**
334 * Consider a field new for 90 days after the release date.
335 *
336 * @since 6.8.3
337 *
338 * @param string $type
339 * @return bool
340 */
341 private static function field_is_new( $type ) {
342 if ( 'ranking' === $type ) {
343 $ranking_release_date = '2024-03-12';
344 $three_months_after_release = gmdate( 'Y-m-d', strtotime( $ranking_release_date . ' + 90 days' ) );
345 return gmdate( 'Y-m-d' ) < $three_months_after_release;
346 }
347 return false;
348 }
349
350 /**
351 * @since 4.0
352 */
353 public static function all_field_selection() {
354 $pro_field_selection = self::pro_field_selection();
355 return array_merge( $pro_field_selection, self::field_selection() );
356 }
357
358 /**
359 * Create a field.
360 *
361 * @param array $values
362 * @param bool $return
363 * @return int|false
364 */
365 public static function create( $values, $return = true ) {
366 global $wpdb, $frm_duplicate_ids;
367
368 $new_values = array();
369 $key = isset( $values['field_key'] ) ? $values['field_key'] : $values['name'];
370 $new_values['field_key'] = FrmAppHelper::get_unique_key( $key, $wpdb->prefix . 'frm_fields', 'field_key' );
371
372 $values = FrmAppHelper::maybe_filter_array( $values, array( 'name', 'description' ) );
373
374 foreach ( array( 'name', 'description', 'type', 'default_value' ) as $col ) {
375 $new_values[ $col ] = $values[ $col ];
376 }
377
378 $new_values['options'] = self::maybe_filter_options( $values['options'] );
379 $new_values['field_order'] = isset( $values['field_order'] ) ? (int) $values['field_order'] : null;
380 $new_values['required'] = isset( $values['required'] ) ? (int) $values['required'] : 0;
381 $new_values['form_id'] = isset( $values['form_id'] ) ? (int) $values['form_id'] : null;
382 $new_values['field_options'] = $values['field_options'];
383 $new_values['created_at'] = current_time( 'mysql', 1 );
384
385 if ( isset( $values['id'] ) ) {
386 $frm_duplicate_ids[ $values['field_key'] ] = $new_values['field_key'];
387 $new_values = apply_filters( 'frm_duplicated_field', $new_values );
388 }
389
390 self::preserve_format_option_backslashes( $new_values );
391
392 foreach ( $new_values as $k => $v ) {
393 if ( is_array( $v ) ) {
394 if ( $k === 'default_value' ) {
395 $new_values[ $k ] = FrmAppHelper::maybe_json_encode( $v );
396 } else {
397 $new_values[ $k ] = serialize( $v );
398 }
399 }
400 unset( $k, $v );
401 }
402
403 $query_results = $wpdb->insert( $wpdb->prefix . 'frm_fields', $new_values );
404
405 if ( ! $query_results ) {
406 return false;
407 }
408
409 self::delete_form_transient( $new_values['form_id'] );
410 $new_id = $wpdb->insert_id;
411
412 if ( ! $return ) {
413 return false;
414 }
415
416 if ( isset( $values['id'] ) ) {
417 $frm_duplicate_ids[ $values['id'] ] = $new_id;
418 }
419
420 return $new_id;
421 }
422
423 /**
424 * @since 5.0.08
425 *
426 * @param array $options
427 * @return array
428 */
429 private static function maybe_filter_options( $options ) {
430 return FrmAppHelper::maybe_filter_array( $options, array( 'custom_html' ) );
431 }
432
433 /**
434 * Process the field duplication.
435 *
436 * @since 5.0.05
437 */
438 public static function duplicate_single_field( $field_id, $form_id ) {
439 $copy_field = self::getOne( $field_id );
440 if ( ! $copy_field ) {
441 return false;
442 }
443
444 do_action( 'frm_duplicate_field', $copy_field, $form_id );
445 do_action( 'frm_duplicate_field_' . $copy_field->type, $copy_field, $form_id );
446
447 $values = array(
448 'id' => $copy_field->id,
449 );
450 FrmFieldsHelper::fill_field( $values, $copy_field, $copy_field->form_id );
451 $values = apply_filters( 'frm_prepare_single_field_for_duplication', $values );
452
453 $field_id = self::create( $values );
454
455 /**
456 * Fires after duplicating a field.
457 *
458 * @since 5.0.04
459 *
460 * @param array $args {
461 * The arguments.
462 *
463 * @type int $field_id New field ID.
464 * @type array $values Values before inserting.
465 * @type object $copy_field Copy field data.
466 * @type int $form_id Form ID.
467 * }
468 */
469 do_action( 'frm_after_duplicate_field', compact( 'field_id', 'values', 'copy_field', 'form_id' ) );
470
471 return compact( 'field_id', 'values' );
472 }
473
474 public static function duplicate( $old_form_id, $form_id, $copy_keys = false, $blog_id = false ) {
475 global $frm_duplicate_ids;
476
477 $where = array(
478 array(
479 'or' => 1,
480 'fi.form_id' => $old_form_id,
481 'fr.parent_form_id' => $old_form_id,
482 ),
483 );
484 $fields = self::getAll( $where, 'field_order', '', $blog_id );
485
486 foreach ( (array) $fields as $field ) {
487 $new_key = $copy_keys ? $field->field_key : '';
488 if ( $copy_keys && substr( $field->field_key, - 1 ) == 2 ) {
489 $new_key = rtrim( $new_key, 2 );
490 }
491
492 $values = array();
493 FrmFieldsHelper::fill_field( $values, $field, $form_id, $new_key );
494
495 // If this is a repeating section, create new form
496 if ( self::is_repeating_field( $field ) ) {
497 // create the repeatable form
498 $new_repeat_form_id = apply_filters(
499 'frm_create_repeat_form',
500 0,
501 array(
502 'parent_form_id' => $form_id,
503 'field_name' => $field->name,
504 )
505 );
506
507 // Save old form_select
508 $old_repeat_form_id = $field->field_options['form_select'];
509
510 // Update form_select for repeating field
511 $values['field_options']['form_select'] = $new_repeat_form_id;
512 }
513
514 // If this is a field inside of a repeating section, associate it with the correct form
515 if ( $field->form_id != $old_form_id && isset( $old_repeat_form_id ) && isset( $new_repeat_form_id ) && $field->form_id == $old_repeat_form_id ) {
516 $values['form_id'] = $new_repeat_form_id;
517 }
518
519 $values['description'] = FrmFieldsHelper::switch_field_ids( $values['description'] );
520
521 $values = apply_filters( 'frm_duplicated_field', $values );
522 $new_id = self::create( $values );
523 $frm_duplicate_ids[ $field->id ] = $new_id;
524 $frm_duplicate_ids[ $field->field_key ] = $new_id;
525 unset( $field );
526 }//end foreach
527 }
528
529 public static function update( $id, $values ) {
530 global $wpdb;
531
532 $id = absint( $id );
533 $values = FrmAppHelper::maybe_filter_array( $values, array( 'name', 'description' ) );
534
535 if ( isset( $values['field_key'] ) ) {
536 $values['field_key'] = FrmAppHelper::get_unique_key( $values['field_key'], $wpdb->prefix . 'frm_fields', 'field_key', $id );
537 }
538
539 if ( isset( $values['required'] ) ) {
540 $values['required'] = (int) $values['required'];
541 }
542
543 self::preserve_format_option_backslashes( $values );
544
545 if ( isset( $values['type'] ) ) {
546 if ( 'dropdown' === $values['type'] ) {
547 // To avoid conflicts with security plugins the value "dropdown" is sent for select fields.
548 // This is because "select" gets matched for SQL injection attempts.
549 $values['type'] = 'select';
550 }
551
552 /**
553 * @since 6.9 The Field ID param was added.
554 *
555 * @param array $values
556 * @param int $id Field ID.
557 */
558 $values = apply_filters( 'frm_clean_' . $values['type'] . '_field_options_before_update', $values, $id );
559
560 if ( $values['type'] === 'hidden' && isset( $values['field_options'] ) && isset( $values['field_options']['clear_on_focus'] ) ) {
561 // don't keep the old placeholder setting for hidden fields
562 $values['field_options']['clear_on_focus'] = 0;
563 }
564 }
565
566 // serialize array values
567 foreach ( array( 'field_options', 'options' ) as $opt ) {
568 if ( isset( $values[ $opt ] ) && is_array( $values[ $opt ] ) ) {
569 if ( 'field_options' === $opt ) {
570 $values[ $opt ] = self::maybe_filter_options( $values[ $opt ] );
571 }
572 $values[ $opt ] = serialize( $values[ $opt ] );
573 }
574 }
575 if ( isset( $values['default_value'] ) && is_array( $values['default_value'] ) ) {
576 $values['default_value'] = json_encode( $values['default_value'] );
577 }
578
579 $query_results = $wpdb->update( $wpdb->prefix . 'frm_fields', $values, array( 'id' => $id ) );
580
581 $form_id = 0;
582 if ( isset( $values['form_id'] ) ) {
583 $form_id = absint( $values['form_id'] );
584 } else {
585 $field = self::getOne( $id );
586 if ( $field ) {
587 $form_id = $field->form_id;
588 }
589 unset( $field );
590 }
591 unset( $values );
592
593 if ( $query_results ) {
594 wp_cache_delete( $id, 'frm_field' );
595 if ( $form_id ) {
596 self::delete_form_transient( $form_id );
597 }
598 }
599
600 return $query_results;
601 }
602
603 /**
604 * Keep backslashes in the phone format option
605 *
606 * @since 2.0.8
607 *
608 * @param array $values Pass by reference.
609 */
610 private static function preserve_format_option_backslashes( &$values ) {
611 if ( isset( $values['field_options']['format'] ) ) {
612 $values['field_options']['format'] = FrmAppHelper::preserve_backslashes( $values['field_options']['format'] );
613 }
614 }
615
616 public static function destroy( $id ) {
617 global $wpdb;
618
619 do_action( 'frm_before_destroy_field', $id );
620
621 wp_cache_delete( $id, 'frm_field' );
622 $field = self::getOne( $id );
623 if ( ! $field ) {
624 return false;
625 }
626
627 self::delete_form_transient( $field->form_id );
628
629 $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_item_metas WHERE field_id=%d', $id ) );
630
631 return $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_fields WHERE id=%d', $id ) );
632 }
633
634 public static function delete_form_transient( $form_id ) {
635 $form_id = absint( $form_id );
636 delete_transient( 'frm_form_fields_' . $form_id . 'excludeinclude' );
637 delete_transient( 'frm_form_fields_' . $form_id . 'includeinclude' );
638 delete_transient( 'frm_form_fields_' . $form_id . 'includeexclude' );
639 delete_transient( 'frm_form_fields_' . $form_id . 'excludeexclude' );
640
641 global $wpdb;
642 $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->options . ' WHERE option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s', '_transient_timeout_frm_form_fields_' . $form_id . 'ex%', '_transient_frm_form_fields_' . $form_id . 'ex%', '_transient_timeout_frm_form_fields_' . $form_id . 'in%', '_transient_frm_form_fields_' . $form_id . 'in%' ) );
643
644 FrmDb::cache_delete_group( 'frm_field' );
645
646 $form = FrmForm::getOne( $form_id );
647 if ( $form && $form->parent_form_id && $form->parent_form_id != $form_id ) {
648 self::delete_form_transient( $form->parent_form_id );
649 }
650 }
651
652 /**
653 * If $field is numeric, get the field object
654 */
655 public static function maybe_get_field( &$field ) {
656 if ( ! is_object( $field ) ) {
657 $field = self::getOne( $field );
658 }
659 }
660
661 /**
662 * @param string|int $id The field id or key.
663 * @param bool $filter When true, run the frm_field filter.
664 */
665 public static function getOne( $id, $filter = false ) {
666 if ( empty( $id ) ) {
667 return null;
668 }
669
670 global $wpdb;
671
672 $where = is_numeric( $id ) ? 'id=%d' : 'field_key=%s';
673 $query = $wpdb->prepare( 'SELECT * FROM ' . $wpdb->prefix . 'frm_fields WHERE ' . $where, $id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
674
675 $results = FrmDb::check_cache( $id, 'frm_field', $query, 'get_row', 0 );
676
677 if ( empty( $results ) ) {
678 self::filter_field( $filter, $results );
679 return $results;
680 }
681
682 if ( is_numeric( $id ) ) {
683 FrmDb::set_cache( $results->field_key, $results, 'frm_field' );
684 } elseif ( $results ) {
685 FrmDb::set_cache( $results->id, $results, 'frm_field' );
686 }
687
688 self::prepare_options( $results );
689 self::filter_field( $filter, $results );
690
691 return wp_unslash( $results );
692 }
693
694 /**
695 * @since 3.06.01
696 * @param bool $filter When true, run the frm_field filter.
697 * @param object $results
698 */
699 private static function filter_field( $filter, &$results ) {
700 if ( $filter ) {
701 /**
702 * @since 3.06.01
703 */
704 $results = apply_filters( 'frm_field', $results );
705 }
706 }
707
708 /**
709 * Get the field type by key or id
710 *
711 * @param int|string $id The field id or key.
712 * @param mixed $col The name of the column in the fields database table.
713 */
714 public static function get_type( $id, $col = 'type' ) {
715 $field = FrmDb::check_cache( $id, 'frm_field' );
716 if ( $field ) {
717 $type = $field->{$col};
718 } else {
719 $where = array(
720 'or' => 1,
721 'id' => $id,
722 'field_key' => $id,
723 );
724 $type = FrmDb::get_var( 'frm_fields', $where, $col );
725 }
726
727 return $type;
728 }
729
730 public static function get_all_types_in_form( $form_id, $type, $limit = '', $inc_sub = 'exclude' ) {
731 if ( ! $form_id ) {
732 return array();
733 }
734
735 $results = self::get_fields_from_transients(
736 $form_id,
737 array(
738 'inc_embed' => $inc_sub,
739 'inc_repeat' => $inc_sub,
740 )
741 );
742 if ( ! empty( $results ) ) {
743 $fields = array();
744 $count = 0;
745 foreach ( $results as $result ) {
746 if ( $type != $result->type ) {
747 continue;
748 }
749
750 $fields[ $result->id ] = $result;
751 ++$count;
752 if ( $limit == 1 ) {
753 $fields = $result;
754 break;
755 }
756
757 if ( ! empty( $limit ) && $count >= $limit ) {
758 break;
759 }
760
761 unset( $result );
762 }
763
764 return wp_unslash( $fields );
765 }//end if
766
767 self::$use_cache = false;
768
769 $where = array(
770 'fi.form_id' => (int) $form_id,
771 'fi.type' => $type,
772 );
773 self::maybe_include_repeating_fields( $inc_sub, $where );
774 $results = self::getAll( $where, 'field_order', $limit );
775 self::$use_cache = true;
776 self::include_sub_fields( $results, $inc_sub, $type, $form_id );
777
778 return $results;
779 }
780
781 public static function get_all_for_form( $form_id, $limit = '', $inc_embed = 'exclude', $inc_repeat = 'include' ) {
782 if ( ! (int) $form_id ) {
783 return array();
784 }
785
786 $results = self::get_fields_from_transients( $form_id, compact( 'inc_embed', 'inc_repeat' ) );
787 if ( ! empty( $results ) ) {
788 if ( empty( $limit ) ) {
789 return $results;
790 }
791
792 $fields = array();
793 $count = 0;
794 foreach ( $results as $result ) {
795 ++$count;
796 $fields[ $result->id ] = $result;
797 if ( ! empty( $limit ) && $count >= $limit ) {
798 break;
799 }
800 }
801
802 return $fields;
803 }
804
805 self::$use_cache = false;
806
807 $where = array( 'fi.form_id' => absint( $form_id ) );
808 self::maybe_include_repeating_fields( $inc_repeat, $where );
809 $results = self::getAll( $where, 'field_order', $limit );
810
811 self::$use_cache = true;
812
813 self::include_sub_fields( $results, $inc_embed, 'all', $form_id );
814
815 if ( empty( $limit ) ) {
816 self::set_field_transient( $results, $form_id, 0, compact( 'inc_embed', 'inc_repeat' ) );
817 }
818
819 return $results;
820 }
821
822 /**
823 * If repeating fields should be included, adjust $where accordingly
824 *
825 * @param string $inc_repeat
826 * @param array $where Pass by reference.
827 */
828 private static function maybe_include_repeating_fields( $inc_repeat, &$where ) {
829 if ( $inc_repeat == 'include' ) {
830 $form_id = $where['fi.form_id'];
831 $where[] = array(
832 'or' => 1,
833 'fi.form_id' => $form_id,
834 'fr.parent_form_id' => $form_id,
835 );
836 unset( $where['fi.form_id'] );
837 }
838 }
839
840 public static function include_sub_fields( &$results, $inc_embed, $type = 'all', $form_id = '' ) {
841 $no_sub_forms = empty( $results ) && $type === 'all';
842 if ( 'include' != $inc_embed || $no_sub_forms ) {
843 return;
844 }
845
846 $form_fields = $results;
847 $should_get_subforms = ( $type !== 'all' && $type !== 'form' && ! empty( $form_id ) );
848 if ( $should_get_subforms ) {
849 $form_fields = self::get_all_types_in_form( $form_id, 'form' );
850 }
851
852 $index_offset = 1;
853 foreach ( $form_fields as $k => $field ) {
854 if ( 'form' != $field->type || ! isset( $field->field_options['form_select'] ) ) {
855 continue;
856 }
857
858 if ( $type == 'all' ) {
859 $sub_fields = self::get_all_for_form( $field->field_options['form_select'] );
860 } else {
861 $sub_fields = self::get_all_types_in_form( $field->field_options['form_select'], $type );
862 }
863
864 if ( ! empty( $sub_fields ) ) {
865 $index = $k + $index_offset;
866 $index_offset += count( $sub_fields );
867 array_splice( $results, $index, 0, $sub_fields );
868 }
869 unset( $field, $sub_fields );
870 }
871 }
872
873 public static function getAll( $where = array(), $order_by = '', $limit = '', $blog_id = false ) {
874 $cache_key = FrmAppHelper::maybe_json_encode( $where ) . $order_by . 'l' . $limit . 'b' . $blog_id;
875 if ( self::$use_cache ) {
876 // make sure old cache doesn't get saved as a transient
877 $results = wp_cache_get( $cache_key, 'frm_field' );
878 if ( false !== $results ) {
879 return wp_unslash( $results );
880 }
881 }
882
883 global $wpdb;
884
885 if ( $blog_id && is_multisite() ) {
886 global $wpmuBaseTablePrefix;
887 if ( $wpmuBaseTablePrefix ) {
888 $prefix = $wpmuBaseTablePrefix . $blog_id . '_';
889 } else {
890 $prefix = $wpdb->get_blog_prefix( $blog_id );
891 }
892
893 $table_name = $prefix . 'frm_fields';
894 $form_table_name = $prefix . 'frm_forms';
895 } else {
896 $table_name = $wpdb->prefix . 'frm_fields';
897 $form_table_name = $wpdb->prefix . 'frm_forms';
898 }
899
900 if ( ! empty( $order_by ) && strpos( $order_by, 'ORDER BY' ) === false ) {
901 $order_by = ' ORDER BY ' . $order_by;
902 }
903
904 $limit = FrmDb::esc_limit( $limit );
905
906 $query = "SELECT fi.*, fr.name as form_name FROM {$table_name} fi LEFT OUTER JOIN {$form_table_name} fr ON fi.form_id=fr.id";
907 $query_type = ( $limit == ' LIMIT 1' || $limit == 1 ) ? 'row' : 'results';
908
909 if ( is_array( $where ) ) {
910 $args = array(
911 'order_by' => $order_by,
912 'limit' => $limit,
913 );
914 $results = FrmDb::get_var( $table_name . ' fi LEFT OUTER JOIN ' . $form_table_name . ' fr ON fi.form_id=fr.id', $where, 'fi.*, fr.name as form_name', $args, '', $query_type );
915 } else {
916 // if the query is not an array, then it has already been prepared
917 $query .= FrmDb::prepend_and_or_where( ' WHERE ', $where ) . $order_by . $limit;
918
919 $function_name = ( $query_type == 'row' ) ? 'get_row' : 'get_results';
920 $results = $wpdb->$function_name( $query );
921 }
922 unset( $where );
923
924 self::format_field_results( $results );
925
926 FrmDb::set_cache( $cache_key, $results, 'frm_field' );
927
928 return wp_unslash( $results );
929 }
930
931 /**
932 * @since 2.0.8
933 */
934 private static function format_field_results( &$results ) {
935 if ( is_array( $results ) ) {
936 foreach ( $results as $r_key => $result ) {
937 FrmDb::set_cache( $result->id, $result, 'frm_field' );
938 FrmDb::set_cache( $result->field_key, $result, 'frm_field' );
939
940 self::prepare_options( $result );
941 $results[ $r_key ]->field_options = $result->field_options;
942 $results[ $r_key ]->options = $result->options;
943 $results[ $r_key ]->default_value = $result->default_value;
944
945 unset( $r_key, $result );
946 }
947 } elseif ( $results ) {
948 FrmDb::set_cache( $results->id, $results, 'frm_field' );
949 FrmDb::set_cache( $results->field_key, $results, 'frm_field' );
950
951 self::prepare_options( $results );
952 }
953 }
954
955 /**
956 * Unserialize all the serialized field data
957 *
958 * @since 2.0
959 */
960 private static function prepare_options( &$results ) {
961 FrmAppHelper::unserialize_or_decode( $results->field_options );
962 FrmAppHelper::unserialize_or_decode( $results->options );
963
964 // Allow a single box to be checked for the default value.
965 $before = $results->default_value;
966 FrmAppHelper::unserialize_or_decode( $results->default_value );
967 if ( $before === $results->default_value && ! is_array( $before ) && strpos( $before, '["' ) === 0 ) {
968 $results->default_value = FrmAppHelper::maybe_json_decode( $results->default_value );
969 }
970 }
971
972 /**
973 * If a form has too many fields, thay won't all save into a single transient.
974 * We'll break them into groups of 200
975 *
976 * @since 2.0.1
977 */
978 private static function get_fields_from_transients( $form_id, $args ) {
979 $fields = array();
980 self::get_next_transient( $fields, 'frm_form_fields_' . $form_id . $args['inc_embed'] . $args['inc_repeat'] );
981
982 return $fields;
983 }
984
985 /**
986 * Called by get_fields_from_transients
987 *
988 * @since 2.0.1
989 */
990 private static function get_next_transient( &$fields, $base_name, $next = 0 ) {
991 $name = $next ? $base_name . $next : $base_name;
992 $next_fields = get_transient( $name );
993
994 if ( $next_fields ) {
995 $fields = array_merge( $fields, $next_fields );
996
997 if ( count( $next_fields ) >= self::$transient_size ) {
998 // if this transient is full, check for another
999 ++$next;
1000 self::get_next_transient( $fields, $base_name, $next );
1001 }
1002 }
1003 }
1004
1005 /**
1006 * Save the transients in chunks for large forms
1007 *
1008 * @since 2.0.1
1009 */
1010 private static function set_field_transient( &$fields, $form_id, $next = 0, $args = array() ) {
1011 $base_name = 'frm_form_fields_' . $form_id . $args['inc_embed'] . $args['inc_repeat'];
1012 $field_chunks = array_chunk( $fields, self::$transient_size );
1013
1014 foreach ( $field_chunks as $field ) {
1015 $name = $next ? $base_name . $next : $base_name;
1016 $set = set_transient( $name, $field, 60 * 60 * 6 );
1017 if ( ! $set ) {
1018 // the transient didn't save
1019 if ( $name != $base_name ) {
1020 // if the first saved an others fail, this will show an incmoplete form
1021 self::delete_form_transient( $form_id );
1022 }
1023
1024 return;
1025 }
1026
1027 ++$next;
1028 }
1029 }
1030
1031 public static function is_no_save_field( $type ) {
1032 return in_array( $type, self::no_save_fields() );
1033 }
1034
1035 public static function no_save_fields() {
1036 return array( 'divider', 'end_divider', 'captcha', 'break', 'html', 'form', 'summary', FrmSubmitHelper::FIELD_TYPE );
1037 }
1038
1039 /**
1040 * Check if this field can hold an array of values
1041 *
1042 * @since 2.0.9
1043 *
1044 * @param array|object $field
1045 *
1046 * @return boolean
1047 */
1048 public static function is_field_with_multiple_values( $field ) {
1049 if ( ! $field ) {
1050 return false;
1051 }
1052
1053 $field_type = self::get_original_field_type( $field );
1054
1055 $is_multi_value_field = (
1056 self::is_checkbox( $field ) ||
1057 $field_type == 'address' ||
1058 self::is_multiple_select( $field )
1059 );
1060
1061 return $is_multi_value_field;
1062 }
1063
1064 /**
1065 * @since 3.0
1066 * @return string
1067 */
1068 public static function get_field_type( $field ) {
1069 return is_array( $field ) ? $field['type'] : $field->type;
1070 }
1071
1072 /**
1073 * @since 3.0
1074 * @return string
1075 */
1076 public static function get_original_field_type( $field ) {
1077 $field_type = self::get_field_type( $field );
1078 $original_type = self::get_option( $field, 'original_type' );
1079
1080 if ( ! empty( $original_type ) && $original_type != $field_type ) {
1081 // Check the original type for arrays.
1082 $field_type = $original_type;
1083 }
1084
1085 return $field_type;
1086 }
1087
1088 /**
1089 * Check if this is a multiselect dropdown field
1090 *
1091 * @since 2.0.9
1092 * @return boolean
1093 */
1094 public static function is_multiple_select( $field ) {
1095 $field_type = self::get_field_type( $field );
1096 $is_multiple = self::is_option_true( $field, 'multiple' ) && self::is_field_type( $field, 'select' ) && $field_type !== 'hidden';
1097
1098 return apply_filters( 'frm_is_multiple_select', $is_multiple, $field );
1099 }
1100
1101 /**
1102 * Check if a field is read only. Read only can be set in the field options,
1103 * but disabled with the shortcode options
1104 *
1105 * @since 2.0.9
1106 */
1107 public static function is_read_only( $field ) {
1108 global $frm_vars;
1109
1110 return ( self::is_option_true( $field, 'read_only' ) && ( ! isset( $frm_vars['readonly'] ) || $frm_vars['readonly'] != 'disabled' ) );
1111 }
1112
1113 /**
1114 * @since 2.0.9
1115 */
1116 public static function is_required( $field ) {
1117 $required = ( $field['required'] != '0' );
1118 $required = apply_filters( 'frm_is_field_required', $required, $field );
1119
1120 return $required;
1121 }
1122
1123 /**
1124 * @since 2.0.9
1125 */
1126 public static function is_option_true( $field, $option ) {
1127 if ( is_array( $field ) ) {
1128 return self::is_option_true_in_array( $field, $option );
1129 } else {
1130 return self::is_option_true_in_object( $field, $option );
1131 }
1132 }
1133
1134 /**
1135 * @since 2.0.9
1136 */
1137 public static function is_option_empty( $field, $option ) {
1138 if ( is_array( $field ) ) {
1139 return self::is_option_empty_in_array( $field, $option );
1140 } else {
1141 return self::is_option_empty_in_object( $field, $option );
1142 }
1143 }
1144
1145 public static function is_option_true_in_array( $field, $option ) {
1146 return isset( $field[ $option ] ) && $field[ $option ];
1147 }
1148
1149 public static function is_option_true_in_object( $field, $option ) {
1150 return isset( $field->field_options[ $option ] ) && $field->field_options[ $option ];
1151 }
1152
1153 public static function is_option_empty_in_array( $field, $option ) {
1154 return ! isset( $field[ $option ] ) || empty( $field[ $option ] );
1155 }
1156
1157 public static function is_option_empty_in_object( $field, $option ) {
1158 return ! isset( $field->field_options[ $option ] ) || empty( $field->field_options[ $option ] );
1159 }
1160
1161 /**
1162 * @param stdClass $field
1163 * @param string $option
1164 * @return bool
1165 */
1166 public static function is_option_value_in_object( $field, $option ) {
1167 return isset( $field->field_options[ $option ] ) && $field->field_options[ $option ] != '';
1168 }
1169
1170 /**
1171 * @since 2.0.18
1172 */
1173 public static function get_option( $field, $option ) {
1174 if ( is_array( $field ) ) {
1175 $option = self::get_option_in_array( $field, $option );
1176 } else {
1177 $option = self::get_option_in_object( $field, $option );
1178 }
1179
1180 return $option;
1181 }
1182
1183 public static function get_option_in_array( $field, $option ) {
1184
1185 if ( isset( $field[ $option ] ) ) {
1186 $this_option = $field[ $option ];
1187 } elseif ( isset( $field['field_options'] ) && is_array( $field['field_options'] ) && isset( $field['field_options'][ $option ] ) ) {
1188 $this_option = $field['field_options'][ $option ];
1189 } else {
1190 $this_option = '';
1191 }
1192
1193 return $this_option;
1194 }
1195
1196 public static function get_option_in_object( $field, $option ) {
1197 return isset( $field->field_options[ $option ] ) ? $field->field_options[ $option ] : '';
1198 }
1199
1200 /**
1201 * @since 2.0.09
1202 */
1203 public static function is_repeating_field( $field ) {
1204 if ( is_array( $field ) ) {
1205 $is_repeating_field = ( 'divider' == $field['type'] );
1206 } else {
1207 $is_repeating_field = ( 'divider' == $field->type );
1208 }
1209
1210 return ( $is_repeating_field && self::is_option_true( $field, 'repeat' ) );
1211 }
1212
1213 /**
1214 * @param string $key
1215 *
1216 * @return int field id
1217 */
1218 public static function get_id_by_key( $key ) {
1219 $id = FrmDb::get_var( 'frm_fields', array( 'field_key' => sanitize_title( $key ) ) );
1220
1221 return (int) $id;
1222 }
1223
1224 /**
1225 * @param string $id
1226 *
1227 * @return null|string
1228 */
1229 public static function get_key_by_id( $id ) {
1230 return FrmDb::get_var( 'frm_fields', array( 'id' => $id ), 'field_key' );
1231 }
1232
1233 public static function is_image( $field ) {
1234 $type = self::get_field_type( $field );
1235
1236 return ( $type == 'url' && self::get_option( $field, 'show_image' ) );
1237 }
1238
1239 /**
1240 * Check if field is radio or Dynamic radio
1241 *
1242 * @since 3.0
1243 *
1244 * @param array|object $field
1245 *
1246 * @return boolean true if field type is radio or Dynamic radio
1247 */
1248 public static function is_radio( $field ) {
1249 return self::is_field_type( $field, 'radio' );
1250 }
1251
1252 /**
1253 * Check if field is checkbox or Dynamic checkbox
1254 *
1255 * @since 3.0
1256 *
1257 * @param array|object $field
1258 *
1259 * @return boolean true if field type is checkbox or Dynamic checkbox
1260 */
1261 public static function is_checkbox( $field ) {
1262 return self::is_field_type( $field, 'checkbox' );
1263 }
1264
1265 /**
1266 * Check if field is checkbox or radio
1267 *
1268 * @since 3.0
1269 *
1270 * @param array|object $field
1271 * @param string $is_type Options include radio, checkbox, text.
1272 *
1273 * @return boolean true if field type is checkbox or Dynamic checkbox
1274 */
1275 public static function is_field_type( $field, $is_type ) {
1276 $field_type = self::get_original_field_type( $field );
1277 $data_type = self::get_option( $field, 'data_type' );
1278
1279 $is_field_type = (
1280 $is_type === $field_type ||
1281 ( 'data' === $field_type && $is_type === $data_type ) ||
1282 ( 'lookup' === $field_type && $is_type === $data_type ) ||
1283 ( 'product' === $field_type && $is_type === $data_type )
1284 );
1285
1286 /**
1287 * When a field type is checked, allow individual fields
1288 * to set the type.
1289 *
1290 * @since 4.04
1291 */
1292 return apply_filters( 'frm_is_field_type', $is_field_type, compact( 'field', 'is_type' ) );
1293 }
1294
1295 /**
1296 * Checks if the given field array is a combo field.
1297 *
1298 * @since 4.10.02
1299 *
1300 * @param array $field Field array.
1301 * @return bool
1302 */
1303 public static function is_combo_field( $field ) {
1304 $field_type_obj = FrmFieldFactory::get_field_factory( $field );
1305
1306 return ! empty( $field_type_obj->is_combo_field );
1307 }
1308 }
1309