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

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