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

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

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