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

1,392 lines 40.3 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_url( $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_url( $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_url( $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_url( $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 false|string 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 * @return array
354 */
355 public static function all_field_selection() {
356 $pro_field_selection = self::pro_field_selection();
357 return array_merge( $pro_field_selection, self::field_selection() );
358 }
359
360 /**
361 * Create a field.
362 *
363 * @param array $values
364 * @param bool $return
365 * @return false|int
366 */
367 public static function create( $values, $return = true ) {
368 global $wpdb, $frm_duplicate_ids;
369
370 $new_values = array();
371 $key = isset( $values['field_key'] ) ? $values['field_key'] : $values['name'];
372 $new_values['field_key'] = FrmAppHelper::get_unique_key( $key, $wpdb->prefix . 'frm_fields', 'field_key' );
373
374 $values = FrmAppHelper::maybe_filter_array( $values, array( 'name', 'description' ) );
375
376 foreach ( array( 'name', 'description', 'type', 'default_value' ) as $col ) {
377 $new_values[ $col ] = $values[ $col ];
378 }
379
380 $new_values['options'] = self::maybe_filter_options( $values['options'] );
381 $new_values['field_order'] = isset( $values['field_order'] ) ? (int) $values['field_order'] : null;
382 $new_values['required'] = isset( $values['required'] ) ? (int) $values['required'] : 0;
383 $new_values['form_id'] = isset( $values['form_id'] ) ? (int) $values['form_id'] : null;
384 $new_values['field_options'] = $values['field_options'];
385 $new_values['created_at'] = current_time( 'mysql', 1 );
386
387 if ( isset( $values['id'] ) ) {
388 $frm_duplicate_ids[ $values['field_key'] ] = $new_values['field_key'];
389 $new_values = apply_filters( 'frm_duplicated_field', $new_values );
390 }
391
392 self::preserve_format_option_backslashes( $new_values );
393
394 foreach ( $new_values as $k => $v ) {
395 if ( is_array( $v ) ) {
396 if ( $k === 'default_value' ) {
397 $new_values[ $k ] = FrmAppHelper::maybe_json_encode( $v );
398 } else {
399 $new_values[ $k ] = serialize( $v );
400 }
401 }
402 unset( $k, $v );
403 }
404
405 $query_results = $wpdb->insert( $wpdb->prefix . 'frm_fields', $new_values );
406
407 if ( ! $query_results ) {
408 return false;
409 }
410
411 self::delete_form_transient( $new_values['form_id'] );
412 $new_id = $wpdb->insert_id;
413
414 if ( ! $return ) {
415 return false;
416 }
417
418 if ( isset( $values['id'] ) ) {
419 $frm_duplicate_ids[ $values['id'] ] = $new_id;
420 }
421
422 return $new_id;
423 }
424
425 /**
426 * @since 5.0.08
427 *
428 * @param array $options
429 * @return array
430 */
431 private static function maybe_filter_options( $options ) {
432 return FrmAppHelper::maybe_filter_array( $options, array( 'custom_html' ) );
433 }
434
435 /**
436 * Process the field duplication.
437 *
438 * @since 5.0.05
439 */
440 public static function duplicate_single_field( $field_id, $form_id ) {
441 $copy_field = self::getOne( $field_id );
442 if ( ! $copy_field ) {
443 return false;
444 }
445
446 do_action( 'frm_duplicate_field', $copy_field, $form_id );
447 do_action( 'frm_duplicate_field_' . $copy_field->type, $copy_field, $form_id );
448
449 $values = array(
450 'id' => $copy_field->id,
451 );
452 FrmFieldsHelper::fill_field( $values, $copy_field, $copy_field->form_id );
453 $values = apply_filters( 'frm_prepare_single_field_for_duplication', $values );
454
455 $field_id = self::create( $values );
456
457 /**
458 * Fires after duplicating a field.
459 *
460 * @since 5.0.04
461 *
462 * @param array $args {
463 * The arguments.
464 *
465 * @type int $field_id New field ID.
466 * @type array $values Values before inserting.
467 * @type object $copy_field Copy field data.
468 * @type int $form_id Form ID.
469 * }
470 */
471 do_action( 'frm_after_duplicate_field', compact( 'field_id', 'values', 'copy_field', 'form_id' ) );
472
473 return compact( 'field_id', 'values' );
474 }
475
476 public static function duplicate( $old_form_id, $form_id, $copy_keys = false, $blog_id = false ) {
477 global $frm_duplicate_ids;
478
479 $where = array(
480 array(
481 'or' => 1,
482 'fi.form_id' => $old_form_id,
483 'fr.parent_form_id' => $old_form_id,
484 ),
485 );
486 $fields = self::getAll( $where, 'field_order', '', $blog_id );
487
488 foreach ( (array) $fields as $field ) {
489 $new_key = $copy_keys ? $field->field_key : '';
490 if ( $copy_keys && substr( $field->field_key, - 1 ) == 2 ) {
491 $new_key = rtrim( $new_key, 2 );
492 }
493
494 $values = array();
495 FrmFieldsHelper::fill_field( $values, $field, $form_id, $new_key );
496
497 // If this is a repeating section, create new form
498 if ( self::is_repeating_field( $field ) ) {
499 // create the repeatable form
500 $new_repeat_form_id = apply_filters(
501 'frm_create_repeat_form',
502 0,
503 array(
504 'parent_form_id' => $form_id,
505 'field_name' => $field->name,
506 )
507 );
508
509 // Save old form_select
510 $old_repeat_form_id = $field->field_options['form_select'];
511
512 // Update form_select for repeating field
513 $values['field_options']['form_select'] = $new_repeat_form_id;
514 }
515
516 // If this is a field inside of a repeating section, associate it with the correct form
517 if ( $field->form_id != $old_form_id && isset( $old_repeat_form_id ) && isset( $new_repeat_form_id ) && $field->form_id == $old_repeat_form_id ) {
518 $values['form_id'] = $new_repeat_form_id;
519 }
520
521 $values['description'] = FrmFieldsHelper::switch_field_ids( $values['description'] );
522
523 $values = apply_filters( 'frm_duplicated_field', $values );
524 $new_id = self::create( $values );
525 $frm_duplicate_ids[ $field->id ] = $new_id;
526 $frm_duplicate_ids[ $field->field_key ] = $new_id;
527 unset( $field );
528 }//end foreach
529 }
530
531 /**
532 * @param int|string $id
533 * @param array $values
534 * @return false|int
535 */
536 public static function update( $id, $values ) {
537 global $wpdb;
538
539 $id = absint( $id );
540 $values = FrmAppHelper::maybe_filter_array( $values, array( 'name', 'description' ) );
541
542 if ( isset( $values['field_key'] ) ) {
543 $values['field_key'] = FrmAppHelper::get_unique_key( $values['field_key'], $wpdb->prefix . 'frm_fields', 'field_key', $id );
544 }
545
546 if ( isset( $values['required'] ) ) {
547 $values['required'] = (int) $values['required'];
548 }
549
550 self::preserve_format_option_backslashes( $values );
551
552 if ( isset( $values['type'] ) ) {
553 if ( 'dropdown' === $values['type'] ) {
554 // To avoid conflicts with security plugins the value "dropdown" is sent for select fields.
555 // This is because "select" gets matched for SQL injection attempts.
556 $values['type'] = 'select';
557 }
558
559 /**
560 * @since 6.9 The Field ID param was added.
561 *
562 * @param array $values
563 * @param int $id Field ID.
564 */
565 $values = apply_filters( 'frm_clean_' . $values['type'] . '_field_options_before_update', $values, $id );
566
567 if ( $values['type'] === 'hidden' && isset( $values['field_options'] ) && isset( $values['field_options']['clear_on_focus'] ) ) {
568 // don't keep the old placeholder setting for hidden fields
569 $values['field_options']['clear_on_focus'] = 0;
570 }
571 }
572
573 // serialize array values
574 foreach ( array( 'field_options', 'options' ) as $opt ) {
575 if ( isset( $values[ $opt ] ) && is_array( $values[ $opt ] ) ) {
576 if ( 'field_options' === $opt ) {
577 $values[ $opt ] = self::maybe_filter_options( $values[ $opt ] );
578 }
579 $values[ $opt ] = serialize( $values[ $opt ] );
580 }
581 }
582 if ( isset( $values['default_value'] ) && is_array( $values['default_value'] ) ) {
583 $values['default_value'] = json_encode( $values['default_value'] );
584 }
585
586 $query_results = $wpdb->update( $wpdb->prefix . 'frm_fields', $values, array( 'id' => $id ) );
587
588 $form_id = 0;
589 if ( isset( $values['form_id'] ) ) {
590 $form_id = absint( $values['form_id'] );
591 } else {
592 $field = self::getOne( $id );
593 if ( $field ) {
594 $form_id = $field->form_id;
595 }
596 unset( $field );
597 }
598 unset( $values );
599
600 if ( $query_results ) {
601 wp_cache_delete( $id, 'frm_field' );
602 if ( $form_id ) {
603 self::delete_form_transient( $form_id );
604 }
605 }
606
607 return $query_results;
608 }
609
610 /**
611 * Keep backslashes in the phone format option
612 *
613 * @since 2.0.8
614 *
615 * @param array $values Pass by reference.
616 */
617 private static function preserve_format_option_backslashes( &$values ) {
618 if ( isset( $values['field_options']['format'] ) ) {
619 $values['field_options']['format'] = FrmAppHelper::preserve_backslashes( $values['field_options']['format'] );
620 }
621 }
622
623 public static function destroy( $id ) {
624 global $wpdb;
625
626 do_action( 'frm_before_destroy_field', $id );
627
628 wp_cache_delete( $id, 'frm_field' );
629 $field = self::getOne( $id );
630 if ( ! $field ) {
631 return false;
632 }
633
634 self::delete_form_transient( $field->form_id );
635
636 $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_item_metas WHERE field_id=%d', $id ) );
637
638 return $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_fields WHERE id=%d', $id ) );
639 }
640
641 public static function delete_form_transient( $form_id ) {
642 $form_id = absint( $form_id );
643 delete_transient( 'frm_form_fields_' . $form_id . 'excludeinclude' );
644 delete_transient( 'frm_form_fields_' . $form_id . 'includeinclude' );
645 delete_transient( 'frm_form_fields_' . $form_id . 'includeexclude' );
646 delete_transient( 'frm_form_fields_' . $form_id . 'excludeexclude' );
647
648 global $wpdb;
649 $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%' ) );
650
651 FrmDb::cache_delete_group( 'frm_field' );
652
653 $form = FrmForm::getOne( $form_id );
654 if ( $form && $form->parent_form_id && $form->parent_form_id != $form_id ) {
655 self::delete_form_transient( $form->parent_form_id );
656 }
657 }
658
659 /**
660 * If $field is numeric, get the field object
661 *
662 * @param int|object|string $field
663 * @return void
664 */
665 public static function maybe_get_field( &$field ) {
666 if ( ! is_object( $field ) ) {
667 $field = self::getOne( $field );
668 }
669 }
670
671 /**
672 * @param int|string $id The field id or key.
673 * @param bool $filter When true, run the frm_field filter.
674 */
675 public static function getOne( $id, $filter = false ) {
676 if ( empty( $id ) ) {
677 return null;
678 }
679
680 global $wpdb;
681
682 $where = is_numeric( $id ) ? 'id=%d' : 'field_key=%s';
683 $query = $wpdb->prepare( 'SELECT * FROM ' . $wpdb->prefix . 'frm_fields WHERE ' . $where, $id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
684
685 $results = FrmDb::check_cache( $id, 'frm_field', $query, 'get_row', 0 );
686
687 if ( empty( $results ) ) {
688 self::filter_field( $filter, $results );
689 return $results;
690 }
691
692 if ( is_numeric( $id ) ) {
693 FrmDb::set_cache( $results->field_key, $results, 'frm_field' );
694 } elseif ( $results ) {
695 FrmDb::set_cache( $results->id, $results, 'frm_field' );
696 }
697
698 self::prepare_options( $results );
699 self::filter_field( $filter, $results );
700
701 return wp_unslash( $results );
702 }
703
704 /**
705 * @since 3.06.01
706 * @param bool $filter When true, run the frm_field filter.
707 * @param object $results
708 */
709 private static function filter_field( $filter, &$results ) {
710 if ( $filter ) {
711 /**
712 * @since 3.06.01
713 */
714 $results = apply_filters( 'frm_field', $results );
715 }
716 }
717
718 /**
719 * Get the field type by key or id
720 *
721 * @param int|string $id The field id or key.
722 * @param mixed $col The name of the column in the fields database table.
723 */
724 public static function get_type( $id, $col = 'type' ) {
725 $field = FrmDb::check_cache( $id, 'frm_field' );
726 if ( $field ) {
727 $type = $field->{$col};
728 } else {
729 $where = array(
730 'or' => 1,
731 'id' => $id,
732 'field_key' => $id,
733 );
734 $type = FrmDb::get_var( 'frm_fields', $where, $col );
735 }
736
737 return $type;
738 }
739
740 /**
741 * @param int|string $form_id
742 * @param string $type
743 * @param int|string $limit
744 * @param string $inc_sub
745 */
746 public static function get_all_types_in_form( $form_id, $type, $limit = '', $inc_sub = 'exclude' ) {
747 if ( ! $form_id ) {
748 return array();
749 }
750
751 $results = self::get_fields_from_transients(
752 $form_id,
753 array(
754 'inc_embed' => $inc_sub,
755 'inc_repeat' => $inc_sub,
756 )
757 );
758 if ( ! empty( $results ) ) {
759 $fields = array();
760 $count = 0;
761 foreach ( $results as $result ) {
762 if ( $type != $result->type ) {
763 continue;
764 }
765
766 $fields[ $result->id ] = $result;
767 ++$count;
768 if ( $limit == 1 ) {
769 $fields = $result;
770 break;
771 }
772
773 if ( ! empty( $limit ) && $count >= $limit ) {
774 break;
775 }
776
777 unset( $result );
778 }
779
780 return wp_unslash( $fields );
781 }//end if
782
783 self::$use_cache = false;
784
785 $where = array(
786 'fi.form_id' => (int) $form_id,
787 'fi.type' => $type,
788 );
789 self::maybe_include_repeating_fields( $inc_sub, $where );
790 $results = self::getAll( $where, 'field_order', $limit );
791 self::$use_cache = true;
792 self::include_sub_fields( $results, $inc_sub, $type, $form_id );
793
794 return $results;
795 }
796
797 /**
798 * @param int|string $form_id
799 * @param int|string $limit
800 * @param string $inc_embed
801 * @param string $inc_repeat
802 * @return array
803 */
804 public static function get_all_for_form( $form_id, $limit = '', $inc_embed = 'exclude', $inc_repeat = 'include' ) {
805 if ( ! (int) $form_id ) {
806 return array();
807 }
808
809 $results = self::get_fields_from_transients( $form_id, compact( 'inc_embed', 'inc_repeat' ) );
810 if ( ! empty( $results ) ) {
811 if ( empty( $limit ) ) {
812 return $results;
813 }
814
815 $fields = array();
816 $count = 0;
817 foreach ( $results as $result ) {
818 ++$count;
819 $fields[ $result->id ] = $result;
820 if ( ! empty( $limit ) && $count >= $limit ) {
821 break;
822 }
823 }
824
825 return $fields;
826 }
827
828 self::$use_cache = false;
829
830 $where = array( 'fi.form_id' => absint( $form_id ) );
831 self::maybe_include_repeating_fields( $inc_repeat, $where );
832 $results = self::getAll( $where, 'field_order', $limit );
833
834 self::$use_cache = true;
835
836 self::include_sub_fields( $results, $inc_embed, 'all', $form_id );
837
838 if ( empty( $limit ) ) {
839 self::set_field_transient( $results, $form_id, 0, compact( 'inc_embed', 'inc_repeat' ) );
840 }
841
842 return $results;
843 }
844
845 /**
846 * If repeating fields should be included, adjust $where accordingly
847 *
848 * @param string $inc_repeat
849 * @param array $where Pass by reference.
850 */
851 private static function maybe_include_repeating_fields( $inc_repeat, &$where ) {
852 if ( $inc_repeat === 'include' ) {
853 $form_id = $where['fi.form_id'];
854 $where[] = array(
855 'or' => 1,
856 'fi.form_id' => $form_id,
857 'fr.parent_form_id' => $form_id,
858 );
859 unset( $where['fi.form_id'] );
860 }
861 }
862
863 public static function include_sub_fields( &$results, $inc_embed, $type = 'all', $form_id = '' ) {
864 $no_sub_forms = empty( $results ) && $type === 'all';
865 if ( 'include' != $inc_embed || $no_sub_forms ) {
866 return;
867 }
868
869 $form_fields = $results;
870 $should_get_subforms = ( $type !== 'all' && $type !== 'form' && ! empty( $form_id ) );
871 if ( $should_get_subforms ) {
872 $form_fields = self::get_all_types_in_form( $form_id, 'form' );
873 }
874
875 $index_offset = 1;
876 foreach ( $form_fields as $k => $field ) {
877 if ( 'form' != $field->type || ! isset( $field->field_options['form_select'] ) ) {
878 continue;
879 }
880
881 if ( $type === 'all' ) {
882 $sub_fields = self::get_all_for_form( $field->field_options['form_select'] );
883 } else {
884 $sub_fields = self::get_all_types_in_form( $field->field_options['form_select'], $type );
885 }
886
887 if ( ! empty( $sub_fields ) ) {
888 $index = $k + $index_offset;
889 $index_offset += count( $sub_fields );
890 array_splice( $results, $index, 0, $sub_fields );
891 }
892 unset( $field, $sub_fields );
893 }
894 }
895
896 public static function getAll( $where = array(), $order_by = '', $limit = '', $blog_id = false ) {
897 $cache_key = FrmAppHelper::maybe_json_encode( $where ) . $order_by . 'l' . $limit . 'b' . $blog_id;
898 if ( self::$use_cache ) {
899 // make sure old cache doesn't get saved as a transient
900 $results = wp_cache_get( $cache_key, 'frm_field' );
901 if ( false !== $results ) {
902 return wp_unslash( $results );
903 }
904 }
905
906 global $wpdb;
907
908 if ( $blog_id && is_multisite() ) {
909 global $wpmuBaseTablePrefix;
910 if ( $wpmuBaseTablePrefix ) {
911 $prefix = $wpmuBaseTablePrefix . $blog_id . '_';
912 } else {
913 $prefix = $wpdb->get_blog_prefix( $blog_id );
914 }
915
916 $table_name = $prefix . 'frm_fields';
917 $form_table_name = $prefix . 'frm_forms';
918 } else {
919 $table_name = $wpdb->prefix . 'frm_fields';
920 $form_table_name = $wpdb->prefix . 'frm_forms';
921 }
922
923 if ( ! empty( $order_by ) && strpos( $order_by, 'ORDER BY' ) === false ) {
924 $order_by = ' ORDER BY ' . $order_by;
925 }
926
927 $limit = FrmDb::esc_limit( $limit );
928
929 $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";
930 $query_type = $limit === ' LIMIT 1' || $limit == 1 ? 'row' : 'results';
931
932 if ( is_array( $where ) ) {
933 $args = array(
934 'order_by' => $order_by,
935 'limit' => $limit,
936 );
937 $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 );
938 } else {
939 // if the query is not an array, then it has already been prepared
940 $query .= FrmDb::prepend_and_or_where( ' WHERE ', $where ) . $order_by . $limit;
941
942 $function_name = $query_type === 'row' ? 'get_row' : 'get_results';
943 $results = $wpdb->$function_name( $query );
944 }
945 unset( $where );
946
947 self::format_field_results( $results );
948
949 FrmDb::set_cache( $cache_key, $results, 'frm_field' );
950
951 return wp_unslash( $results );
952 }
953
954 /**
955 * @since 2.0.8
956 */
957 private static function format_field_results( &$results ) {
958 if ( is_array( $results ) ) {
959 foreach ( $results as $r_key => $result ) {
960 FrmDb::set_cache( $result->id, $result, 'frm_field' );
961 FrmDb::set_cache( $result->field_key, $result, 'frm_field' );
962
963 self::prepare_options( $result );
964 $results[ $r_key ]->field_options = $result->field_options;
965 $results[ $r_key ]->options = $result->options;
966 $results[ $r_key ]->default_value = $result->default_value;
967
968 unset( $r_key, $result );
969 }
970 } elseif ( $results ) {
971 FrmDb::set_cache( $results->id, $results, 'frm_field' );
972 FrmDb::set_cache( $results->field_key, $results, 'frm_field' );
973
974 self::prepare_options( $results );
975 }
976 }
977
978 /**
979 * Unserialize all the serialized field data
980 *
981 * @since 2.0
982 */
983 private static function prepare_options( &$results ) {
984 FrmAppHelper::unserialize_or_decode( $results->field_options );
985 FrmAppHelper::unserialize_or_decode( $results->options );
986
987 // Allow a single box to be checked for the default value.
988 $before = $results->default_value;
989 FrmAppHelper::unserialize_or_decode( $results->default_value );
990 if ( $before === $results->default_value && ! is_array( $before ) && strpos( $before, '["' ) === 0 ) {
991 $results->default_value = FrmAppHelper::maybe_json_decode( $results->default_value );
992 }
993 }
994
995 /**
996 * If a form has too many fields, thay won't all save into a single transient.
997 * We'll break them into groups of 200
998 *
999 * @since 2.0.1
1000 */
1001 private static function get_fields_from_transients( $form_id, $args ) {
1002 $fields = array();
1003 self::get_next_transient( $fields, 'frm_form_fields_' . $form_id . $args['inc_embed'] . $args['inc_repeat'] );
1004
1005 return $fields;
1006 }
1007
1008 /**
1009 * Called by get_fields_from_transients
1010 *
1011 * @since 2.0.1
1012 */
1013 private static function get_next_transient( &$fields, $base_name, $next = 0 ) {
1014 $name = $next ? $base_name . $next : $base_name;
1015 $next_fields = get_transient( $name );
1016
1017 if ( $next_fields ) {
1018 $fields = array_merge( $fields, $next_fields );
1019
1020 if ( count( $next_fields ) >= self::$transient_size ) {
1021 // if this transient is full, check for another
1022 ++$next;
1023 self::get_next_transient( $fields, $base_name, $next );
1024 }
1025 }
1026 }
1027
1028 /**
1029 * Save the transients in chunks for large forms
1030 *
1031 * @since 2.0.1
1032 */
1033 private static function set_field_transient( &$fields, $form_id, $next = 0, $args = array() ) {
1034 $base_name = 'frm_form_fields_' . $form_id . $args['inc_embed'] . $args['inc_repeat'];
1035 $field_chunks = array_chunk( $fields, self::$transient_size );
1036
1037 foreach ( $field_chunks as $field ) {
1038 $name = $next ? $base_name . $next : $base_name;
1039 $set = set_transient( $name, $field, 60 * 60 * 6 );
1040 if ( ! $set ) {
1041 // the transient didn't save
1042 if ( $name != $base_name ) {
1043 // if the first saved an others fail, this will show an incmoplete form
1044 self::delete_form_transient( $form_id );
1045 }
1046
1047 return;
1048 }
1049
1050 ++$next;
1051 }
1052 }
1053
1054 /**
1055 * @param string $type
1056 * @return bool
1057 */
1058 public static function is_no_save_field( $type ) {
1059 return in_array( $type, self::no_save_fields(), true );
1060 }
1061
1062 /**
1063 * @return string[]
1064 */
1065 public static function no_save_fields() {
1066 return array( 'divider', 'end_divider', 'captcha', 'break', 'html', 'form', 'summary', FrmSubmitHelper::FIELD_TYPE );
1067 }
1068
1069 /**
1070 * Check if this field can hold an array of values
1071 *
1072 * @since 2.0.9
1073 *
1074 * @param array|object $field
1075 *
1076 * @return bool
1077 */
1078 public static function is_field_with_multiple_values( $field ) {
1079 if ( ! $field ) {
1080 return false;
1081 }
1082
1083 $field_type = self::get_original_field_type( $field );
1084
1085 $is_multi_value_field = (
1086 self::is_checkbox( $field ) ||
1087 $field_type === 'address' ||
1088 self::is_multiple_select( $field )
1089 );
1090
1091 return $is_multi_value_field;
1092 }
1093
1094 /**
1095 * @since 3.0
1096 * @param array|object $field
1097 * @return string
1098 */
1099 public static function get_field_type( $field ) {
1100 return is_array( $field ) ? $field['type'] : $field->type;
1101 }
1102
1103 /**
1104 * @since 3.0
1105 * @return string
1106 */
1107 public static function get_original_field_type( $field ) {
1108 $field_type = self::get_field_type( $field );
1109 $original_type = self::get_option( $field, 'original_type' );
1110
1111 if ( ! empty( $original_type ) && $original_type != $field_type ) {
1112 // Check the original type for arrays.
1113 $field_type = $original_type;
1114 }
1115
1116 return $field_type;
1117 }
1118
1119 /**
1120 * Check if this is a multiselect dropdown field
1121 *
1122 * @since 2.0.9
1123 * @return bool
1124 */
1125 public static function is_multiple_select( $field ) {
1126 $field_type = self::get_field_type( $field );
1127 $is_multiple = self::is_option_true( $field, 'multiple' ) && self::is_field_type( $field, 'select' ) && $field_type !== 'hidden';
1128
1129 return apply_filters( 'frm_is_multiple_select', $is_multiple, $field );
1130 }
1131
1132 /**
1133 * Check if a field is read only. Read only can be set in the field options,
1134 * but disabled with the shortcode options
1135 *
1136 * @since 2.0.9
1137 *
1138 * @param array|object $field
1139 * @return bool
1140 */
1141 public static function is_read_only( $field ) {
1142 global $frm_vars;
1143 return self::is_option_true( $field, 'read_only' ) && ( ! isset( $frm_vars['readonly'] ) || $frm_vars['readonly'] !== 'disabled' );
1144 }
1145
1146 /**
1147 * @since 2.0.9
1148 *
1149 * @param array $field
1150 * @return bool
1151 */
1152 public static function is_required( $field ) {
1153 $required = $field['required'] != '0';
1154
1155 /**
1156 * @param bool $required
1157 * @param array $field
1158 */
1159 $required = (bool) apply_filters( 'frm_is_field_required', $required, $field );
1160
1161 return $required;
1162 }
1163
1164 /**
1165 * @since 2.0.9
1166 *
1167 * @param array|object $field
1168 * @param string $option
1169 * @return bool
1170 */
1171 public static function is_option_true( $field, $option ) {
1172 if ( is_array( $field ) ) {
1173 return self::is_option_true_in_array( $field, $option );
1174 }
1175 return self::is_option_true_in_object( $field, $option );
1176 }
1177
1178 /**
1179 * @since 2.0.9
1180 *
1181 * @param array|object $field
1182 * @param string $option
1183 * @return bool
1184 */
1185 public static function is_option_empty( $field, $option ) {
1186 if ( is_array( $field ) ) {
1187 return self::is_option_empty_in_array( $field, $option );
1188 }
1189 return self::is_option_empty_in_object( $field, $option );
1190 }
1191
1192 /**
1193 * @param array $field
1194 * @param string $option
1195 * @return bool
1196 */
1197 public static function is_option_true_in_array( $field, $option ) {
1198 return ! empty( $field[ $option ] );
1199 }
1200
1201 /**
1202 * @param object $field
1203 * @param string $option
1204 * @return bool
1205 */
1206 public static function is_option_true_in_object( $field, $option ) {
1207 return isset( $field->field_options[ $option ] ) && $field->field_options[ $option ];
1208 }
1209
1210 /**
1211 * @param array $field
1212 * @param string $option
1213 * @return bool
1214 */
1215 public static function is_option_empty_in_array( $field, $option ) {
1216 return empty( $field[ $option ] );
1217 }
1218
1219 /**
1220 * @param object $field
1221 * @param string $option
1222 * @return bool
1223 */
1224 public static function is_option_empty_in_object( $field, $option ) {
1225 return empty( $field->field_options[ $option ] );
1226 }
1227
1228 /**
1229 * @param stdClass $field
1230 * @param string $option
1231 * @return bool
1232 */
1233 public static function is_option_value_in_object( $field, $option ) {
1234 return isset( $field->field_options[ $option ] ) && $field->field_options[ $option ] != '';
1235 }
1236
1237 /**
1238 * @since 2.0.18
1239 *
1240 * @param array|object $field
1241 * @param string $option
1242 * @return mixed
1243 */
1244 public static function get_option( $field, $option ) {
1245 if ( is_array( $field ) ) {
1246 $option = self::get_option_in_array( $field, $option );
1247 } else {
1248 $option = self::get_option_in_object( $field, $option );
1249 }
1250
1251 return $option;
1252 }
1253
1254 /**
1255 * @param array $field
1256 * @param string $option
1257 * @return mixed
1258 */
1259 public static function get_option_in_array( $field, $option ) {
1260 if ( isset( $field[ $option ] ) ) {
1261 $this_option = $field[ $option ];
1262 } elseif ( isset( $field['field_options'] ) && is_array( $field['field_options'] ) && isset( $field['field_options'][ $option ] ) ) {
1263 $this_option = $field['field_options'][ $option ];
1264 } else {
1265 $this_option = '';
1266 }
1267
1268 return $this_option;
1269 }
1270
1271 /**
1272 * @param object $field
1273 * @param string $option
1274 * @return mixed
1275 */
1276 public static function get_option_in_object( $field, $option ) {
1277 return isset( $field->field_options[ $option ] ) ? $field->field_options[ $option ] : '';
1278 }
1279
1280 /**
1281 * @since 2.0.09
1282 *
1283 * @param array|object $field
1284 * @return bool
1285 */
1286 public static function is_repeating_field( $field ) {
1287 if ( is_array( $field ) ) {
1288 $is_repeating_field = ( 'divider' === $field['type'] );
1289 } else {
1290 $is_repeating_field = ( 'divider' === $field->type );
1291 }
1292
1293 return $is_repeating_field && self::is_option_true( $field, 'repeat' );
1294 }
1295
1296 /**
1297 * @param string $key
1298 *
1299 * @return int field id
1300 */
1301 public static function get_id_by_key( $key ) {
1302 $id = FrmDb::get_var( 'frm_fields', array( 'field_key' => sanitize_title( $key ) ) );
1303
1304 return (int) $id;
1305 }
1306
1307 /**
1308 * @param string $id
1309 *
1310 * @return string|null
1311 */
1312 public static function get_key_by_id( $id ) {
1313 return FrmDb::get_var( 'frm_fields', array( 'id' => $id ), 'field_key' );
1314 }
1315
1316 public static function is_image( $field ) {
1317 $type = self::get_field_type( $field );
1318
1319 return ( $type === 'url' && self::get_option( $field, 'show_image' ) );
1320 }
1321
1322 /**
1323 * Check if field is radio or Dynamic radio
1324 *
1325 * @since 3.0
1326 *
1327 * @param array|object $field
1328 *
1329 * @return bool true if field type is radio or Dynamic radio
1330 */
1331 public static function is_radio( $field ) {
1332 return self::is_field_type( $field, 'radio' );
1333 }
1334
1335 /**
1336 * Check if field is checkbox or Dynamic checkbox
1337 *
1338 * @since 3.0
1339 *
1340 * @param array|object $field
1341 *
1342 * @return bool true if field type is checkbox or Dynamic checkbox
1343 */
1344 public static function is_checkbox( $field ) {
1345 return self::is_field_type( $field, 'checkbox' );
1346 }
1347
1348 /**
1349 * Check if field is checkbox or radio
1350 *
1351 * @since 3.0
1352 *
1353 * @param array|object $field
1354 * @param string $is_type Options include radio, checkbox, text.
1355 *
1356 * @return bool true if field type is checkbox or Dynamic checkbox
1357 */
1358 public static function is_field_type( $field, $is_type ) {
1359 $field_type = self::get_original_field_type( $field );
1360 $data_type = self::get_option( $field, 'data_type' );
1361
1362 $is_field_type = (
1363 $is_type === $field_type ||
1364 ( 'data' === $field_type && $is_type === $data_type ) ||
1365 ( 'lookup' === $field_type && $is_type === $data_type ) ||
1366 ( 'product' === $field_type && $is_type === $data_type )
1367 );
1368
1369 /**
1370 * When a field type is checked, allow individual fields
1371 * to set the type.
1372 *
1373 * @since 4.04
1374 */
1375 return apply_filters( 'frm_is_field_type', $is_field_type, compact( 'field', 'is_type' ) );
1376 }
1377
1378 /**
1379 * Checks if the given field array is a combo field.
1380 *
1381 * @since 4.10.02
1382 *
1383 * @param array $field Field array.
1384 * @return bool
1385 */
1386 public static function is_combo_field( $field ) {
1387 $field_type_obj = FrmFieldFactory::get_field_factory( $field );
1388
1389 return ! empty( $field_type_obj->is_combo_field );
1390 }
1391 }
1392