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

1,643 lines 49.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 /**
9 * @var bool
10 */
11 public static $use_cache = true;
12
13 /**
14 * @var int
15 */
16 public static $transient_size = 200;
17
18 /**
19 * @return array
20 */
21 public static function field_selection() {
22 $fields = array(
23 'text' => array(
24 'name' => __( 'Text', 'formidable' ),
25 'icon' => 'frm_icon_font frm_text2_icon',
26 ),
27 'textarea' => array(
28 'name' => __( 'Paragraph', 'formidable' ),
29 'icon' => 'frm_icon_font frm_paragraph_icon',
30 ),
31 'checkbox' => array(
32 'name' => __( 'Checkboxes', 'formidable' ),
33 'icon' => 'frm_icon_font frm_check_square_icon',
34 ),
35 'radio' => array(
36 'name' => __( 'Radio Buttons', 'formidable' ),
37 'icon' => 'frm_icon_font frm_radio_checked_icon',
38 ),
39 'select' => array(
40 'name' => __( 'Dropdown', 'formidable' ),
41 'icon' => 'frm_icon_font frm_dropdown_icon',
42 ),
43 'email' => array(
44 'name' => __( 'Email', 'formidable' ),
45 'icon' => 'frm_icon_font frm_email2_icon',
46 ),
47 'url' => array(
48 'name' => __( 'Website/URL', 'formidable' ),
49 'icon' => 'frm_icon_font frm_link2_icon',
50 ),
51 'number' => array(
52 'name' => __( 'Number', 'formidable' ),
53 'icon' => 'frm_icon_font frm_hashtag_icon',
54 ),
55 'name' => array(
56 'name' => __( 'Name', 'formidable' ),
57 'icon' => 'frm_icon_font frm_user_name_icon',
58 ),
59 'phone' => array(
60 'name' => __( 'Phone', 'formidable' ),
61 'icon' => 'frm_icon_font frm_phone_icon',
62 ),
63 'html' => array(
64 'name' => __( 'HTML', 'formidable' ),
65 'icon' => 'frm_icon_font frm_code2_icon',
66 ),
67 'hidden' => array(
68 'name' => __( 'Hidden', 'formidable' ),
69 'icon' => 'frm_icon_font frm_eye_slash2_icon',
70 ),
71 'user_id' => array(
72 'name' => __( 'User ID', 'formidable' ),
73 'icon' => 'frm_icon_font frm_user2_icon',
74 ),
75 'captcha' => array(
76 'name' => self::get_captcha_field_name(),
77 'icon' => 'frm_icon_font frm_shield_check2_icon',
78 ),
79 'credit_card' => array(
80 'name' => __( 'Payment', 'formidable' ),
81 'icon' => 'frm_icon_font frm_credit_card2_icon',
82 ),
83 FrmSubmitHelper::FIELD_TYPE => array(
84 'name' => __( 'Submit', 'formidable' ),
85 'hide' => true,
86 ),
87 FrmFieldGdprHelper::FIELD_TYPE => array(
88 'name' => __( 'GDPR', 'formidable' ),
89 'icon' => 'frm_icon_font frm-gdpr-icon',
90 'hide' => FrmFieldGdprHelper::hide_gdpr_field(),
91 ),
92 );
93
94 /**
95 * @param array $fields
96 */
97 return apply_filters( 'frm_available_fields', $fields );
98 }
99
100 /**
101 * Get the name of the Captcha field based on the global Captcha setting.
102 *
103 * @return string
104 */
105 private static function get_captcha_field_name() {
106 return 'Captcha';
107 }
108
109 /**
110 * @return array
111 */
112 public static function pro_field_selection() {
113 $upsell_images_url = FrmAppHelper::plugin_url() . '/images/upsell/';
114 $fields = array(
115 'file' => array(
116 'name' => __( 'File Upload', 'formidable' ),
117 'icon' => 'frm_icon_font frm_upload3_icon',
118 'message' => __( 'Add file uploads to save time and cut down on back-and-forth. Upgrade to Pro to get Upload fields and more.', 'formidable' ),
119 'upsell_image' => $upsell_images_url . 'file-upload-field-preview.webp',
120 'learn-more' => 'features/wordpress-multiple-file-upload-form',
121 ),
122 'ranking' => array(
123 'name' => __( 'Ranking', 'formidable' ),
124 'icon' => 'frm_icon_font frm_chart_bar_icon frm_show_upgrade',
125 'message' => __( 'Now you can effortlessly gather insights, preferences, and opinions by allowing users to rank options.', 'formidable' ),
126 'upsell_image' => $upsell_images_url . 'ranking-field-preview.webp',
127 'addon' => 'surveys',
128 'learn-more' => 'ranking-survey',
129 ),
130 'rte' => array(
131 'name' => __( 'Rich Text', 'formidable' ),
132 'icon' => 'frm_icon_font frm_align_right_icon',
133 'message' => __( 'Go beyond plain text, let your users format their content with bolding, italics, links, lists and more.', 'formidable' ),
134 'upsell_image' => $upsell_images_url . 'rich-text-field-preview.webp',
135 'learn-more' => 'rich-text',
136 ),
137 'date' => array(
138 'name' => __( 'Date', 'formidable' ),
139 'icon' => 'frm_icon_font frm_calendar2_icon',
140 'message' => __( 'Capture exact calendar dates effortlessly with a sleek pop-up date picker, ensuring clarity and consistency for scheduling, tracking deadlines, and more.', 'formidable' ),
141 'upsell_image' => $upsell_images_url . 'date-field-preview.webp',
142 'learn-more' => 'date',
143 ),
144 'time' => array(
145 'name' => __( 'Time', 'formidable' ),
146 'icon' => 'frm_icon_font frm_clock_icon',
147 'message' => __( 'Precisely log important moments with an intuitive time selection tool, perfect for managing events, availability, or operational hours.', 'formidable' ),
148 'upsell_image' => $upsell_images_url . 'time-field-preview.webp',
149 'learn-more' => 'time',
150 ),
151 'scale' => array(
152 'name' => __( 'Scale', 'formidable' ),
153 'icon' => 'frm_icon_font frm_linear_scale_icon',
154 'message' => esc_html__( 'Easily measure satisfaction or quantity using a simple, clear numerical scale, giving you fast, quantifiable insights into user feedback.', 'formidable' ),
155 'upsell_image' => $upsell_images_url . 'scale-field-preview.webp',
156 'learn-more' => 'scale',
157 ),
158 'star' => array(
159 'name' => __( 'Star Rating', 'formidable' ),
160 'icon' => 'frm_icon_font frm_star2_icon',
161 'message' => esc_html__( 'Capture instant, appealing feedback with a familiar visual star system, providing immediate and engaging quality assessments from your users.', 'formidable' ),
162 'upsell_image' => $upsell_images_url . 'star-field-preview.webp',
163 'learn-more' => 'star-ratings',
164 ),
165 'range' => array(
166 'name' => __( 'Slider', 'formidable' ),
167 'icon' => 'frm_icon_font frm_code_commit_icon',
168 'message' => esc_html__( 'Let users quickly select values within a range using a dynamic, interactive slider, creating a modern and enjoyable data input experience.', 'formidable' ),
169 'upsell_image' => $upsell_images_url . 'slider-field-preview.webp',
170 'learn-more' => 'slider',
171 ),
172 'toggle' => array(
173 'name' => __( 'Toggle', 'formidable' ),
174 'icon' => 'frm_icon_font frm_toggle_on_icon',
175 'message' => esc_html__( 'Quickly and easily create cascading fields, populate fields by search and more.', 'formidable' ),
176 'upsell_image' => $upsell_images_url . 'toggle-field-preview.webp',
177 'learn-more' => 'toggle',
178 ),
179 'data' => array(
180 'name' => __( 'Dynamic', 'formidable' ),
181 'icon' => 'frm_icon_font frm_dynamic_icon',
182 'message' => __( 'Link entries together, dynamic display information, and even make selections from data entered on another form.', 'formidable' ),
183 'upsell_image' => $upsell_images_url . 'dynamic-field-preview.webp',
184 'learn-more' => 'dynamic',
185 ),
186 'lookup' => array(
187 'name' => __( 'Lookup', 'formidable' ),
188 'icon' => 'frm_icon_font frm_search_icon',
189 'message' => esc_html__( 'Link entries together, dynamic display information, and even make selections from data entered on another form.', 'formidable' ),
190 'upsell_image' => $upsell_images_url . 'lookup-field-preview.webp',
191 'learn-more' => 'lookup',
192 ),
193 'divider|repeat' => array(
194 'name' => __( 'Repeater', 'formidable' ),
195 'icon' => 'frm_icon_font frm_refresh_icon',
196 'message' => esc_html__( 'Allow users to add rows of fields dynamically as needed (like for multiple attendees or items), making complex data entry flexible and intuitive.', 'formidable' ),
197 'upsell_image' => $upsell_images_url . 'repeater-field-preview.webp',
198 'learn-more' => 'repeatable-section',
199 ),
200 'end_divider' => array(
201 'name' => __( 'Section Buttons', 'formidable' ),
202 'switch_from' => 'divider',
203 ),
204 'divider' => array(
205 'name' => __( 'Section', 'formidable' ),
206 'icon' => 'frm_icon_font frm_header_icon',
207 'message' => esc_html__( 'Enhance the user experience and improve completion rates by dividing long surveys into clean, manageable steps with distinct pages.', 'formidable' ),
208 'upsell_image' => $upsell_images_url . 'section-field-preview.webp',
209 'learn-more' => 'section-heading',
210 ),
211 'break' => array(
212 'name' => __( 'Page Break', 'formidable' ),
213 'icon' => 'frm_icon_font frm_page_break_icon',
214 'message' => esc_html__( 'Enhance the user experience and improve completion rates by dividing long surveys into clean, manageable steps with distinct pages.', 'formidable' ),
215 'upsell_image' => $upsell_images_url . 'page-break-field-preview.webp',
216 'learn-more' => 'page-breaks',
217 ),
218 'form' => array(
219 'name' => __( 'Embed Form', 'formidable' ),
220 'icon' => 'frm_icon_font frm_file_text2_icon',
221 'message' => esc_html__( 'Seamlessly integrate other existing forms or external content directly inside your current form, creating powerful, interconnected workflows.', 'formidable' ),
222 'upsell_image' => $upsell_images_url . 'embed-form-field-preview.webp',
223 'learn-more' => 'embed-form',
224 ),
225 'likert' => array(
226 'name' => __( 'Likert Scale', 'formidable' ),
227 'icon' => 'frm_icon_font frm_likert_scale frm_show_upgrade',
228 'addon' => 'surveys',
229 'message' => esc_html__( 'Get nuanced, detailed opinions using standardized agreement scales, unlocking deeper insights beyond simple yes/no answers.', 'formidable' ),
230 'upsell_image' => $upsell_images_url . 'likert-field-preview.webp',
231 'learn-more' => 'likert-scale',
232 ),
233 'nps' => array(
234 'name' => __( 'NPS', 'formidable' ),
235 'icon' => 'frm_icon_font frm_nps frm_show_upgrade',
236 'addon' => 'surveys',
237 'message' => esc_html__( 'Directly measure customer loyalty with the industry-standard Net Promoter Score® field, giving you a single, vital metric for business growth.', 'formidable' ),
238 'upsell_image' => $upsell_images_url . 'nps-field-preview.webp',
239 'learn-more' => 'net-promoter-score',
240 ),
241 'password' => array(
242 'name' => __( 'Password', 'formidable' ),
243 'icon' => 'frm_icon_font frm_lock_closed2_icon',
244 'message' => esc_html__( 'Confidently collect sensitive credentials or create secure sections with a dedicated field that masks input for privacy and security.', 'formidable' ),
245 'upsell_image' => $upsell_images_url . 'password-field-preview.webp',
246 'learn-more' => 'password-2',
247 ),
248 'tag' => array(
249 'name' => __( 'Tags', 'formidable' ),
250 'icon' => 'frm_icon_font frm_price_tags2_icon',
251 'message' => esc_html__( 'Facilitate easy categorization and analysis by letting users select or create multiple relevant keywords or labels for their entry.', 'formidable' ),
252 'upsell_image' => $upsell_images_url . 'tags-field-preview.webp',
253 'learn-more' => 'tags',
254 ),
255 // This is no longer a Pro field, but without this here, Pro triggers "undefined index" notices.
256 // Right now it leaves a gap. Maybe we can skip anything without a name or something.
257 'credit_card' => array(
258 'name' => '',
259 'icon' => '',
260 ),
261 'address' => array(
262 'name' => __( 'Address', 'formidable' ),
263 'icon' => 'frm_icon_font frm_location2_icon',
264 'message' => esc_html__( 'Instantly capture location information, simplifying logistics and contact collection. (Or go further with our Geolocation add-on!)', 'formidable' ),
265 'upsell_image' => $upsell_images_url . 'address-field-preview.webp',
266 'learn-more' => 'address',
267 ),
268 'summary' => array(
269 'name' => __( 'Summary', 'formidable' ),
270 'icon' => 'frm_icon_font frm_file_text3_icon',
271 'message' => __( 'Provide users with a clean, final review of all their entered data before submission, reducing errors and improving data quality.', 'formidable' ),
272 'upsell_image' => $upsell_images_url . 'summary-field-preview.webp',
273 'learn-more' => 'summary-review-before-submit',
274 ),
275 'signature' => array(
276 'name' => __( 'Signature', 'formidable' ),
277 'icon' => 'frm_icon_font frm_signature_icon frm_show_upgrade',
278 'addon' => 'signature',
279 'message' => __( 'Capture digital e-signatures directly within your form, making official agreements and authorizations straightforward.', 'formidable' ),
280 'upsell_image' => $upsell_images_url . 'signature-field-preview.webp',
281 'learn-more' => 'formidable-signature',
282 ),
283 'ai' => array(
284 'name' => __( 'AI', 'formidable' ),
285 'icon' => 'frm_icon_font frm-ai-icon frm_show_upgrade',
286 'addon' => 'ai',
287 'message' => __( 'Harness artificial intelligence by taking user input and let the model of your choice create an output, quickly and easily.', 'formidable' ),
288 'upsell_image' => $upsell_images_url . 'ai-field-preview.webp',
289 'learn-more' => 'form-ai',
290 ),
291 'ssa-appointment' => array(
292 'name' => __( 'Appointment', 'formidable' ),
293 'icon' => 'frm_icon_font frm_schedule_icon frm_show_upgrade',
294 'require' => 'Simply Schedule Appointments',
295 'message' => sprintf(
296 /* translators: %1$s: Link opening HTML, %2$s: Link tag closing */
297 esc_html__( 'Appointment fields are an integration with %1$sSimply Schedule Appointments%2$s. Get started now to schedule appointments directly from your forms.', 'formidable' ),
298 '<a class="frm-inline-flex" href="https://simplyscheduleappointments.com/meet/formidable/" target="_blank" rel="noopener">',
299 '</a>'
300 ),
301 'link' => 'https://simplyscheduleappointments.com/meet/formidable/',
302 'upsell_image' => $upsell_images_url . 'appointment-field-preview.webp',
303 'learn-more' => 'simply-schedule-appointments-forms',
304 ),
305 'product' => array(
306 'name' => __( 'Product', 'formidable' ),
307 'icon' => 'frm_icon_font frm_product2_icon',
308 'section' => 'pricing',
309 'message' => __( 'Turn your form into a simple storefront by adding fields to list products, choose quantities, and calculate totals, enabling direct sales.', 'formidable' ),
310 'upsell_image' => $upsell_images_url . 'product-field-preview.webp',
311 'learn-more' => 'features/pricing-fields',
312 ),
313 'quantity' => array(
314 'name' => __( 'Quantity', 'formidable' ),
315 'icon' => 'frm_icon_font frm_quantity_icon',
316 'section' => 'pricing',
317 'message' => __( 'Turn your form into a simple storefront by adding fields to list products, choose quantities, and calculate totals, enabling direct sales.', 'formidable' ),
318 'upsell_image' => $upsell_images_url . 'quantity-field-preview.webp',
319 'learn-more' => 'features/pricing-fields',
320 ),
321 'total' => array(
322 'name' => __( 'Total', 'formidable' ),
323 'icon' => 'frm_icon_font frm_total2_icon',
324 'section' => 'pricing',
325 'message' => __( 'Turn your form into a simple storefront by adding fields to list products, choose quantities, and calculate totals, enabling direct sales.', 'formidable' ),
326 'upsell_image' => $upsell_images_url . 'total-field-preview.webp',
327 'learn-more' => 'features/pricing-fields',
328 ),
329 );
330
331 // Since the signature field may be in a different section, don't show it twice.
332 $lite_fields = self::field_selection();
333
334 if ( isset( $lite_fields['signature'] ) ) {
335 unset( $fields['signature'] );
336 }
337
338 return apply_filters( 'frm_pro_available_fields', $fields );
339 }
340
341 /**
342 * Consider a field new for 90 days after the release date.
343 *
344 * @since 6.8.3
345 *
346 * @param string $type
347 *
348 * @return bool
349 */
350 private static function field_is_new( $type ) {
351 $release_dates = array(
352 'ranking' => '2024-03-12',
353 );
354
355 if ( ! isset( $release_dates[ $type ] ) ) {
356 return false;
357 }
358
359 $release_date = $release_dates[ $type ];
360
361 $three_months_after_release = gmdate( 'Y-m-d', strtotime( $release_date . ' + 90 days' ) );
362 return gmdate( 'Y-m-d' ) < $three_months_after_release;
363 }
364
365 /**
366 * @since 4.0
367 *
368 * @return array
369 */
370 public static function all_field_selection() {
371 $pro_field_selection = self::pro_field_selection();
372 return array_merge( $pro_field_selection, self::field_selection() );
373 }
374
375 /**
376 * Create a field.
377 *
378 * @param array $values
379 * @param bool $return
380 *
381 * @return false|int
382 */
383 public static function create( $values, $return = true ) {
384 global $wpdb, $frm_duplicate_ids;
385
386 $new_values = array();
387 $key = $values['field_key'] ?? $values['name'];
388 $new_values['field_key'] = FrmAppHelper::get_unique_key( $key, $wpdb->prefix . 'frm_fields', 'field_key' );
389
390 $values = FrmAppHelper::maybe_filter_array( $values, array( 'name', 'description' ) );
391
392 foreach ( array( 'name', 'description', 'type', 'default_value' ) as $col ) {
393 if ( isset( $values[ $col ] ) ) {
394 $new_values[ $col ] = $values[ $col ];
395 }
396 }
397
398 $new_values['options'] = self::maybe_filter_options( $values['options'] );
399 $new_values['field_order'] = isset( $values['field_order'] ) ? (int) $values['field_order'] : null;
400 $new_values['required'] = isset( $values['required'] ) ? (int) $values['required'] : 0;
401 $new_values['form_id'] = isset( $values['form_id'] ) ? (int) $values['form_id'] : null;
402 $new_values['field_options'] = $values['field_options'];
403 $new_values['created_at'] = current_time( 'mysql', 1 );
404
405 if ( isset( $values['id'] ) ) {
406 $frm_duplicate_ids[ $values['field_key'] ] = $new_values['field_key'];
407 $new_values = apply_filters( 'frm_duplicated_field', $new_values );
408 }
409
410 self::preserve_format_option_backslashes( $new_values );
411
412 foreach ( $new_values as $k => $v ) {
413 if ( is_array( $v ) ) {
414 if ( $k === 'default_value' ) {
415 $new_values[ $k ] = FrmAppHelper::maybe_json_encode( $v );
416 } else {
417 $new_values[ $k ] = serialize( $v );
418 }
419 }
420 unset( $k, $v );
421 }
422
423 $query_results = $wpdb->insert( $wpdb->prefix . 'frm_fields', $new_values );
424
425 if ( ! $query_results ) {
426 return false;
427 }
428
429 self::delete_form_transient( $new_values['form_id'] );
430 $new_id = $wpdb->insert_id;
431
432 if ( ! $return ) {
433 return false;
434 }
435
436 if ( isset( $values['id'] ) ) {
437 $frm_duplicate_ids[ $values['id'] ] = $new_id;
438 }
439
440 return $new_id;
441 }
442
443 /**
444 * Maybe filter HTML in field options data.
445 * HTML is only filtered when unsafe HTML is disallowed.
446 * See FrmAppHelper::allow_unfiltered_html.
447 *
448 * @since 5.0.08
449 *
450 * @param array $options
451 *
452 * @return array
453 */
454 private static function maybe_filter_options( $options ) {
455 $options = FrmAppHelper::maybe_filter_array( $options, array( 'custom_html' ) );
456
457 if ( ! empty( $options['custom_html'] ) ) {
458 $options['custom_html'] = self::maybe_filter_custom_html_input_attributes( $options['custom_html'] );
459 }
460
461 if ( ! empty( $options['classes'] ) ) {
462 $options['classes'] = implode( ' ', array_map( 'FrmFormsHelper::sanitize_layout_class', explode( ' ', $options['classes'] ) ) );
463 }
464
465 return $options;
466 }
467
468 /**
469 * Prevent users who do not have permission to insert JavaScript attributes in input elements.
470 * This is triggered when a field is updated.
471 *
472 * @since 6.11.2
473 *
474 * @param string $html
475 *
476 * @return string
477 */
478 private static function maybe_filter_custom_html_input_attributes( $html ) {
479 if ( FrmAppHelper::allow_unfiltered_html() ) {
480 return $html;
481 }
482
483 $pattern = get_shortcode_regex( array( 'input' ) );
484 return preg_replace_callback(
485 "/$pattern/",
486 /**
487 * @param array $match Shortcode data.
488 *
489 * @return string
490 */
491 function ( $match ) {
492 $attr = shortcode_parse_atts( $match[3] );
493
494 if ( ! is_array( $attr ) ) {
495 // In old versions of WordPress (older than 6.5), this might not be an array.
496 return '[input]';
497 }
498
499 $safe_atts = array();
500
501 foreach ( $attr as $attr_key => $att ) {
502 if ( ! is_numeric( $attr_key ) ) {
503 // opt=1 without parentheses for example is mapped like 'opt' => 1.
504 $key = $attr_key;
505 $value = $att;
506 } else {
507 // Some data is mapped like 0 => 'placeholder="Placeholder"'.
508 $split = explode( '=', $att, 2 );
509
510 if ( 2 !== count( $split ) ) {
511 continue;
512 }
513
514 $key = trim( $split[0] );
515 $value = trim( $split[1], '"' );
516 }
517
518 if ( FrmAppHelper::input_key_is_safe( $key, 'update' ) ) {
519 $safe_atts[ $key ] = $value;
520 }
521 }//end foreach
522
523 if ( ! $safe_atts ) {
524 return '[input]';
525 }
526
527 return '[input ' . FrmAppHelper::array_to_html_params( $safe_atts ) . ']';
528 },
529 $html
530 );
531 }
532
533 /**
534 * Process the field duplication.
535 *
536 * @since 5.0.05
537 *
538 * @param int|string $field_id Field ID.
539 * @param int|string $form_id Form ID.
540 *
541 * @return array|false
542 */
543 public static function duplicate_single_field( $field_id, $form_id ) {
544 $copy_field = self::getOne( $field_id );
545
546 if ( ! $copy_field ) {
547 return false;
548 }
549
550 do_action( 'frm_duplicate_field', $copy_field, $form_id );
551 do_action( 'frm_duplicate_field_' . $copy_field->type, $copy_field, $form_id );
552
553 $values = array(
554 'id' => $copy_field->id,
555 );
556 FrmFieldsHelper::fill_field( $values, $copy_field, $copy_field->form_id );
557 $values = apply_filters( 'frm_prepare_single_field_for_duplication', $values );
558
559 $field_id = self::create( $values );
560
561 /**
562 * Fires after duplicating a field.
563 *
564 * @since 5.0.04
565 *
566 * @param array $args {
567 * The arguments.
568 *
569 * @type int $field_id New field ID.
570 * @type array $values Values before inserting.
571 * @type object $copy_field Copy field data.
572 * @type int $form_id Form ID.
573 * }
574 */
575 do_action( 'frm_after_duplicate_field', compact( 'field_id', 'values', 'copy_field', 'form_id' ) );
576
577 return compact( 'field_id', 'values' );
578 }
579
580 /**
581 * @param int|string $old_form_id
582 * @param int|string $form_id
583 * @param bool $copy_keys
584 * @param false|int $blog_id
585 *
586 * @return void
587 */
588 public static function duplicate( $old_form_id, $form_id, $copy_keys = false, $blog_id = false ) {
589 global $frm_duplicate_ids;
590
591 $where = array(
592 array(
593 'or' => 1,
594 'fi.form_id' => $old_form_id,
595 'fr.parent_form_id' => $old_form_id,
596 ),
597 );
598 $fields = self::getAll( $where, 'field_order', '', $blog_id );
599
600 foreach ( (array) $fields as $field ) {
601 $new_key = $copy_keys ? $field->field_key : '';
602
603 if ( $copy_keys && substr( $field->field_key, - 1 ) == 2 ) {
604 $new_key = rtrim( $new_key, 2 );
605 }
606
607 $values = array();
608 FrmFieldsHelper::fill_field( $values, $field, $form_id, $new_key );
609
610 // If this is a repeating section, create new form
611 if ( self::is_repeating_field( $field ) ) {
612 // create the repeatable form
613 $new_repeat_form_id = apply_filters(
614 'frm_create_repeat_form',
615 0,
616 array(
617 'parent_form_id' => $form_id,
618 'field_name' => $field->name,
619 )
620 );
621
622 // Save old form_select
623 $old_repeat_form_id = $field->field_options['form_select'];
624
625 // Update form_select for repeating field
626 $values['field_options']['form_select'] = $new_repeat_form_id;
627 }
628
629 // If this is a field inside of a repeating section, associate it with the correct form
630 if ( $field->form_id != $old_form_id && isset( $old_repeat_form_id ) && isset( $new_repeat_form_id ) && $field->form_id == $old_repeat_form_id ) {
631 $values['form_id'] = $new_repeat_form_id;
632 }
633
634 $values['description'] = FrmFieldsHelper::switch_field_ids( $values['description'] );
635
636 $values = apply_filters( 'frm_duplicated_field', $values );
637 $new_id = self::create( $values );
638 $frm_duplicate_ids[ $field->id ] = $new_id;
639 $frm_duplicate_ids[ $field->field_key ] = $new_id;
640 unset( $field );
641 }//end foreach
642 }
643
644 /**
645 * @param int|string $id
646 * @param array $values
647 *
648 * @return false|int
649 */
650 public static function update( $id, $values ) {
651 global $wpdb;
652
653 $id = absint( $id );
654 $values = FrmAppHelper::maybe_filter_array( $values, array( 'name', 'description' ) );
655
656 if ( isset( $values['field_key'] ) ) {
657 $values['field_key'] = FrmAppHelper::get_unique_key( $values['field_key'], $wpdb->prefix . 'frm_fields', 'field_key', $id );
658 }
659
660 if ( isset( $values['required'] ) ) {
661 $values['required'] = (int) $values['required'];
662 }
663
664 self::preserve_format_option_backslashes( $values );
665
666 if ( isset( $values['type'] ) ) {
667 if ( 'dropdown' === $values['type'] ) {
668 // To avoid conflicts with security plugins the value "dropdown" is sent for select fields.
669 // This is because "select" gets matched for SQL injection attempts.
670 $values['type'] = 'select';
671 }
672
673 /**
674 * @since 6.9 The Field ID param was added.
675 *
676 * @param array $values
677 * @param int $id Field ID.
678 */
679 $values = apply_filters( 'frm_clean_' . $values['type'] . '_field_options_before_update', $values, $id );
680
681 if ( $values['type'] === 'hidden' && isset( $values['field_options'] ) && isset( $values['field_options']['clear_on_focus'] ) ) {
682 // don't keep the old placeholder setting for hidden fields
683 $values['field_options']['clear_on_focus'] = 0;
684 }
685 }
686
687 // serialize array values
688 foreach ( array( 'field_options', 'options' ) as $opt ) {
689 if ( isset( $values[ $opt ] ) && is_array( $values[ $opt ] ) ) {
690 if ( 'field_options' === $opt ) {
691 $values[ $opt ] = self::maybe_filter_options( $values[ $opt ] );
692 }
693 $values[ $opt ] = serialize( $values[ $opt ] );
694 }
695 }
696
697 if ( isset( $values['default_value'] ) && is_array( $values['default_value'] ) ) {
698 $values['default_value'] = json_encode( $values['default_value'] );
699 }
700
701 $query_results = $wpdb->update( $wpdb->prefix . 'frm_fields', $values, array( 'id' => $id ) );
702
703 $form_id = 0;
704
705 if ( isset( $values['form_id'] ) ) {
706 $form_id = absint( $values['form_id'] );
707 } else {
708 $field = self::getOne( $id );
709
710 if ( $field ) {
711 $form_id = $field->form_id;
712 }
713 unset( $field );
714 }
715 unset( $values );
716
717 if ( $query_results ) {
718 wp_cache_delete( $id, 'frm_field' );
719
720 if ( $form_id ) {
721 self::delete_form_transient( $form_id );
722 }
723 }
724
725 return $query_results;
726 }
727
728 /**
729 * Keep backslashes in the phone format option
730 *
731 * @since 2.0.8
732 *
733 * @param array $values Pass by reference.
734 *
735 * @return void
736 */
737 private static function preserve_format_option_backslashes( &$values ) {
738 if ( isset( $values['field_options']['format'] ) ) {
739 $values['field_options']['format'] = FrmAppHelper::preserve_backslashes( $values['field_options']['format'] );
740 }
741 }
742
743 /**
744 * @param int|string $id
745 *
746 * @return bool|int
747 */
748 public static function destroy( $id ) {
749 global $wpdb;
750
751 do_action( 'frm_before_destroy_field', $id );
752
753 wp_cache_delete( $id, 'frm_field' );
754 $field = self::getOne( $id );
755
756 if ( ! $field ) {
757 return false;
758 }
759
760 self::delete_form_transient( $field->form_id );
761
762 $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_item_metas WHERE field_id=%d', $id ) );
763
764 return $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_fields WHERE id=%d', $id ) );
765 }
766
767 /**
768 * @param int|string $form_id
769 *
770 * @return void
771 */
772 public static function delete_form_transient( $form_id ) {
773 $form_id = absint( $form_id );
774 delete_transient( 'frm_form_fields_' . $form_id . 'excludeinclude' );
775 delete_transient( 'frm_form_fields_' . $form_id . 'includeinclude' );
776 delete_transient( 'frm_form_fields_' . $form_id . 'includeexclude' );
777 delete_transient( 'frm_form_fields_' . $form_id . 'excludeexclude' );
778
779 global $wpdb;
780 $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%' ) );
781
782 FrmDb::cache_delete_group( 'frm_field' );
783
784 $form = FrmForm::getOne( $form_id );
785
786 if ( $form && $form->parent_form_id && $form->parent_form_id != $form_id ) {
787 self::delete_form_transient( $form->parent_form_id );
788 }
789 }
790
791 /**
792 * If $field is numeric, get the field object
793 *
794 * @param int|object|string $field
795 *
796 * @return void
797 */
798 public static function maybe_get_field( &$field ) {
799 if ( ! is_object( $field ) ) {
800 $field = self::getOne( $field );
801 }
802 }
803
804 /**
805 * @param int|string $id The field id or key.
806 * @param bool $filter When true, run the frm_field filter.
807 *
808 * @return object|null
809 */
810 public static function getOne( $id, $filter = false ) {
811 if ( empty( $id ) ) {
812 return null;
813 }
814
815 global $wpdb;
816
817 $where = is_numeric( $id ) ? 'id=%d' : 'field_key=%s';
818 $query = $wpdb->prepare( 'SELECT * FROM ' . $wpdb->prefix . 'frm_fields WHERE ' . $where, $id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
819
820 $results = FrmDb::check_cache( $id, 'frm_field', $query, 'get_row', 0 );
821
822 if ( empty( $results ) ) {
823 self::filter_field( $filter, $results );
824 return $results;
825 }
826
827 if ( is_numeric( $id ) ) {
828 FrmDb::set_cache( $results->field_key, $results, 'frm_field' );
829 } elseif ( $results ) {
830 FrmDb::set_cache( $results->id, $results, 'frm_field' );
831 }
832
833 self::prepare_options( $results );
834 self::filter_field( $filter, $results );
835
836 return wp_unslash( $results );
837 }
838
839 /**
840 * @since 3.06.01
841 *
842 * @param bool $filter When true, run the frm_field filter.
843 * @param object $results
844 *
845 * @return void
846 */
847 private static function filter_field( $filter, &$results ) {
848 if ( $filter ) {
849 /**
850 * @since 3.06.01
851 */
852 $results = apply_filters( 'frm_field', $results );
853 }
854 }
855
856 /**
857 * Get the field type by key or id
858 *
859 * @param int|string $id The field id or key.
860 * @param mixed $col The name of the column in the fields database table.
861 *
862 * @return mixed
863 */
864 public static function get_type( $id, $col = 'type' ) {
865 $field = FrmDb::check_cache( $id, 'frm_field' );
866
867 if ( $field ) {
868 $type = $field->{$col};
869 } else {
870 if ( is_numeric( $id ) ) {
871 $where = array( 'id' => $id );
872 } else {
873 $where = array( 'field_key' => $id );
874 }
875
876 $type = FrmDb::get_var( 'frm_fields', $where, $col );
877 }
878
879 return $type;
880 }
881
882 /**
883 * @param int|string $form_id
884 * @param string $type
885 * @param int|string $limit
886 * @param string $inc_sub
887 *
888 * @return array|object
889 */
890 public static function get_all_types_in_form( $form_id, $type, $limit = '', $inc_sub = 'exclude' ) {
891 if ( ! $form_id ) {
892 return array();
893 }
894
895 $results = self::get_fields_from_transients(
896 $form_id,
897 array(
898 'inc_embed' => $inc_sub,
899 'inc_repeat' => $inc_sub,
900 )
901 );
902
903 if ( ! empty( $results ) ) {
904 $fields = array();
905 $count = 0;
906
907 foreach ( $results as $result ) {
908 if ( $type != $result->type ) {
909 continue;
910 }
911
912 $fields[ $result->id ] = $result;
913 ++$count;
914
915 if ( $limit == 1 ) {
916 $fields = $result;
917 break;
918 }
919
920 if ( ! empty( $limit ) && $count >= $limit ) {
921 break;
922 }
923
924 unset( $result );
925 }
926
927 return wp_unslash( $fields );
928 }//end if
929
930 self::$use_cache = false;
931
932 $where = array(
933 'fi.form_id' => (int) $form_id,
934 'fi.type' => $type,
935 );
936 self::maybe_include_repeating_fields( $inc_sub, $where );
937 $results = self::getAll( $where, 'field_order', $limit );
938 self::$use_cache = true;
939 self::include_sub_fields( $results, $inc_sub, $type, $form_id );
940
941 return $results;
942 }
943
944 /**
945 * @param int|string $form_id
946 * @param int|string $limit
947 * @param string $inc_embed
948 * @param string $inc_repeat
949 *
950 * @return array
951 */
952 public static function get_all_for_form( $form_id, $limit = '', $inc_embed = 'exclude', $inc_repeat = 'include' ) {
953 if ( ! (int) $form_id ) {
954 return array();
955 }
956
957 $results = self::get_fields_from_transients( $form_id, compact( 'inc_embed', 'inc_repeat' ) );
958
959 if ( ! empty( $results ) ) {
960 if ( empty( $limit ) ) {
961 return $results;
962 }
963
964 $fields = array();
965 $count = 0;
966
967 foreach ( $results as $result ) {
968 ++$count;
969 $fields[ $result->id ] = $result;
970
971 if ( ! empty( $limit ) && $count >= $limit ) {
972 break;
973 }
974 }
975
976 return $fields;
977 }
978
979 self::$use_cache = false;
980
981 $where = array( 'fi.form_id' => absint( $form_id ) );
982 self::maybe_include_repeating_fields( $inc_repeat, $where );
983 $results = self::getAll( $where, 'field_order', $limit );
984
985 self::$use_cache = true;
986
987 self::include_sub_fields( $results, $inc_embed, 'all', $form_id );
988
989 if ( empty( $limit ) ) {
990 self::set_field_transient( $results, $form_id, 0, compact( 'inc_embed', 'inc_repeat' ) );
991 }
992
993 return $results;
994 }
995
996 /**
997 * If repeating fields should be included, adjust $where accordingly
998 *
999 * @param string $inc_repeat
1000 * @param array $where Pass by reference.
1001 *
1002 * @return void
1003 */
1004 private static function maybe_include_repeating_fields( $inc_repeat, &$where ) {
1005 if ( $inc_repeat === 'include' ) {
1006 $form_id = $where['fi.form_id'];
1007 $where[] = array(
1008 'or' => 1,
1009 'fi.form_id' => $form_id,
1010 'fr.parent_form_id' => $form_id,
1011 );
1012 unset( $where['fi.form_id'] );
1013 }
1014 }
1015
1016 /**
1017 * @param array $results
1018 * @param string $inc_embed
1019 * @param string $type
1020 * @param int|string $form_id
1021 *
1022 * @return void
1023 */
1024 public static function include_sub_fields( &$results, $inc_embed, $type = 'all', $form_id = '' ) {
1025 $no_sub_forms = empty( $results ) && $type === 'all';
1026
1027 if ( 'include' != $inc_embed || $no_sub_forms ) {
1028 return;
1029 }
1030
1031 $form_fields = $results;
1032 $should_get_subforms = ( $type !== 'all' && $type !== 'form' && ! empty( $form_id ) );
1033
1034 if ( $should_get_subforms ) {
1035 $form_fields = self::get_all_types_in_form( $form_id, 'form' );
1036 }
1037
1038 $index_offset = 1;
1039
1040 foreach ( $form_fields as $k => $field ) {
1041 if ( 'form' != $field->type || ! isset( $field->field_options['form_select'] ) ) {
1042 continue;
1043 }
1044
1045 if ( $type === 'all' ) {
1046 $sub_fields = self::get_all_for_form( $field->field_options['form_select'] );
1047 } else {
1048 $sub_fields = self::get_all_types_in_form( $field->field_options['form_select'], $type );
1049 }
1050
1051 if ( ! empty( $sub_fields ) ) {
1052 $index = $k + $index_offset;
1053 $index_offset += count( $sub_fields );
1054 array_splice( $results, $index, 0, $sub_fields );
1055 }
1056 unset( $field, $sub_fields );
1057 }
1058 }
1059
1060 /**
1061 * @param array|string $where
1062 * @param string $order_by
1063 * @param string $limit
1064 * @param false|int $blog_id
1065 *
1066 * @return array|object|null
1067 */
1068 public static function getAll( $where = array(), $order_by = '', $limit = '', $blog_id = false ) {
1069 $cache_key = FrmAppHelper::maybe_json_encode( $where ) . $order_by . 'l' . $limit . 'b' . $blog_id;
1070
1071 if ( self::$use_cache ) {
1072 // make sure old cache doesn't get saved as a transient
1073 $results = wp_cache_get( $cache_key, 'frm_field' );
1074
1075 if ( false !== $results ) {
1076 return wp_unslash( $results );
1077 }
1078 }
1079
1080 global $wpdb;
1081
1082 if ( $blog_id && is_multisite() ) {
1083 $prefix = $wpdb->get_blog_prefix( $blog_id );
1084
1085 $table_name = $prefix . 'frm_fields';
1086 $form_table_name = $prefix . 'frm_forms';
1087 } else {
1088 $table_name = $wpdb->prefix . 'frm_fields';
1089 $form_table_name = $wpdb->prefix . 'frm_forms';
1090 }
1091
1092 if ( ! empty( $order_by ) && strpos( $order_by, 'ORDER BY' ) === false ) {
1093 $order_by = ' ORDER BY ' . $order_by;
1094 }
1095
1096 $limit = FrmDb::esc_limit( $limit );
1097
1098 $query = "SELECT fi.*, fr.name as form_name FROM {$table_name} fi JOIN {$form_table_name} fr ON fi.form_id=fr.id";
1099 $query_type = $limit === ' LIMIT 1' || $limit == 1 ? 'row' : 'results';
1100
1101 if ( is_array( $where ) ) {
1102 $args = array(
1103 'order_by' => $order_by,
1104 'limit' => $limit,
1105 );
1106 $results = FrmDb::get_var( $table_name . ' fi JOIN ' . $form_table_name . ' fr ON fi.form_id=fr.id', $where, 'fi.*, fr.name as form_name', $args, '', $query_type );
1107 } else {
1108 // if the query is not an array, then it has already been prepared
1109 $query .= FrmDb::prepend_and_or_where( ' WHERE ', $where ) . $order_by . $limit;
1110
1111 $function_name = $query_type === 'row' ? 'get_row' : 'get_results';
1112 $results = $wpdb->$function_name( $query );
1113 }
1114 unset( $where );
1115
1116 self::format_field_results( $results );
1117
1118 FrmDb::set_cache( $cache_key, $results, 'frm_field' );
1119
1120 return wp_unslash( $results );
1121 }
1122
1123 /**
1124 * @since 2.0.8
1125 *
1126 * @param array|object|null $results Results.
1127 *
1128 * @return void
1129 */
1130 private static function format_field_results( &$results ) {
1131 if ( is_array( $results ) ) {
1132 foreach ( $results as $r_key => $result ) {
1133 self::add_slashes_to_format_before_setting_field_cache( $result );
1134
1135 FrmDb::set_cache( $result->id, $result, 'frm_field' );
1136 FrmDb::set_cache( $result->field_key, $result, 'frm_field' );
1137
1138 self::prepare_options( $result );
1139 $results[ $r_key ]->field_options = $result->field_options;
1140 $results[ $r_key ]->options = $result->options;
1141 $results[ $r_key ]->default_value = $result->default_value;
1142
1143 unset( $r_key, $result );
1144 }
1145 } elseif ( $results ) {
1146 FrmDb::set_cache( $results->id, $results, 'frm_field' );
1147 FrmDb::set_cache( $results->field_key, $results, 'frm_field' );
1148
1149 self::prepare_options( $results );
1150 }
1151 }
1152
1153 /**
1154 * When $result->field_options is an array and not a serialized string there is only a single backslash.
1155 * Cached results are unslashed in FrmField::getAll, so we need to make sure that the cached object has an extra backslash.
1156 * Otherwise the backslash is stripped away on load.
1157 *
1158 * @since 6.15
1159 *
1160 * @param stdClass $result
1161 *
1162 * @return void
1163 */
1164 private static function add_slashes_to_format_before_setting_field_cache( $result ) {
1165 if ( ! isset( $result->field_options ) || ! is_array( $result->field_options ) || empty( $result->field_options['format'] ) ) {
1166 return;
1167 }
1168
1169 $result->field_options['format'] = addslashes( $result->field_options['format'] );
1170 }
1171
1172 /**
1173 * Unserialize all the serialized field data
1174 *
1175 * @since 2.0
1176 *
1177 * @param object $results
1178 *
1179 * @return void
1180 */
1181 private static function prepare_options( &$results ) {
1182 FrmAppHelper::unserialize_or_decode( $results->field_options );
1183 FrmAppHelper::unserialize_or_decode( $results->options );
1184
1185 // Allow a single box to be checked for the default value.
1186 $before = $results->default_value;
1187
1188 $field_object = FrmFieldFactory::get_field_type( $results->type );
1189
1190 if ( $field_object->should_unserialize_value() ) {
1191 FrmAppHelper::unserialize_or_decode( $results->default_value );
1192
1193 if ( $before === $results->default_value && is_string( $before ) && strpos( $before, '["' ) === 0 ) {
1194 $results->default_value = FrmAppHelper::maybe_json_decode( $results->default_value );
1195 }
1196 }
1197 }
1198
1199 /**
1200 * If a form has too many fields, they won't all save into a single transient.
1201 * We'll break them into groups of 200
1202 *
1203 * @since 2.0.1
1204 *
1205 * @param int|string $form_id Form ID.
1206 * @param array $args Additional arguments.
1207 *
1208 * @return array
1209 */
1210 private static function get_fields_from_transients( $form_id, $args ) {
1211 $fields = array();
1212 self::get_next_transient( $fields, 'frm_form_fields_' . $form_id . $args['inc_embed'] . $args['inc_repeat'] );
1213
1214 return $fields;
1215 }
1216
1217 /**
1218 * Called by get_fields_from_transients
1219 *
1220 * @since 2.0.1
1221 *
1222 * @param array $fields Array of fields.
1223 * @param string $base_name Base name.
1224 * @param int $next Next transient number.
1225 *
1226 * @return void
1227 */
1228 private static function get_next_transient( &$fields, $base_name, $next = 0 ) {
1229 $name = $next ? $base_name . $next : $base_name;
1230 $next_fields = get_transient( $name );
1231
1232 if ( $next_fields ) {
1233 $fields = array_merge( $fields, $next_fields );
1234
1235 if ( count( $next_fields ) >= self::$transient_size ) {
1236 // if this transient is full, check for another
1237 ++$next;
1238 self::get_next_transient( $fields, $base_name, $next );
1239 }
1240 }
1241 }
1242
1243 /**
1244 * Save the transients in chunks for large forms
1245 *
1246 * @since 2.0.1
1247 *
1248 * @param array $fields Array of fields.
1249 * @param int|string $form_id Form ID.
1250 * @param int $next Next transient number.
1251 * @param array $args Additional arguments.
1252 *
1253 * @return void
1254 */
1255 private static function set_field_transient( &$fields, $form_id, $next = 0, $args = array() ) {
1256 $base_name = 'frm_form_fields_' . $form_id . $args['inc_embed'] . $args['inc_repeat'];
1257 $field_chunks = array_chunk( $fields, self::$transient_size );
1258
1259 foreach ( $field_chunks as $field ) {
1260 $name = $next ? $base_name . $next : $base_name;
1261 $set = set_transient( $name, $field, 60 * 60 * 6 );
1262
1263 if ( ! $set ) {
1264 // the transient didn't save
1265 if ( $name !== $base_name ) {
1266 // if the first saved an others fail, this will show an incomplete form
1267 self::delete_form_transient( $form_id );
1268 }
1269
1270 return;
1271 }
1272
1273 ++$next;
1274 }
1275 }
1276
1277 /**
1278 * @param string $type
1279 *
1280 * @return bool
1281 */
1282 public static function is_no_save_field( $type ) {
1283 return in_array( $type, self::no_save_fields(), true );
1284 }
1285
1286 /**
1287 * @return string[]
1288 */
1289 public static function no_save_fields() {
1290 return array( 'divider', 'end_divider', 'captcha', 'break', 'html', 'form', 'summary', FrmSubmitHelper::FIELD_TYPE );
1291 }
1292
1293 /**
1294 * Check if this field can hold an array of values
1295 *
1296 * @since 2.0.9
1297 *
1298 * @param array|object $field
1299 *
1300 * @return bool
1301 */
1302 public static function is_field_with_multiple_values( $field ) {
1303 if ( ! $field ) {
1304 return false;
1305 }
1306
1307 $field_type = self::get_original_field_type( $field );
1308
1309 $is_multi_value_field = (
1310 self::is_checkbox( $field ) ||
1311 $field_type === 'address' ||
1312 self::is_multiple_select( $field )
1313 );
1314
1315 return $is_multi_value_field;
1316 }
1317
1318 /**
1319 * @since 3.0
1320 *
1321 * @param array|object $field
1322 *
1323 * @return string
1324 */
1325 public static function get_field_type( $field ) {
1326 return is_array( $field ) ? $field['type'] : $field->type;
1327 }
1328
1329 /**
1330 * @since 3.0
1331 *
1332 * @param array|object $field
1333 *
1334 * @return string
1335 */
1336 public static function get_original_field_type( $field ) {
1337 $field_type = self::get_field_type( $field );
1338 $original_type = self::get_option( $field, 'original_type' );
1339
1340 if ( ! empty( $original_type ) && $original_type != $field_type ) {
1341 // Check the original type for arrays.
1342 $field_type = $original_type;
1343 }
1344
1345 return $field_type;
1346 }
1347
1348 /**
1349 * Check if this is a multiselect dropdown field
1350 *
1351 * @since 2.0.9
1352 *
1353 * @param array|object $field Field object.
1354 *
1355 * @return bool
1356 */
1357 public static function is_multiple_select( $field ) {
1358 $field_type = self::get_field_type( $field );
1359 $is_multiple = self::is_option_true( $field, 'multiple' ) && self::is_field_type( $field, 'select' ) && $field_type !== 'hidden';
1360
1361 return apply_filters( 'frm_is_multiple_select', $is_multiple, $field );
1362 }
1363
1364 /**
1365 * Check if a field is read only. Read only can be set in the field options,
1366 * but disabled with the shortcode options
1367 *
1368 * @since 2.0.9
1369 *
1370 * @param array|object $field
1371 *
1372 * @return bool
1373 */
1374 public static function is_read_only( $field ) {
1375 global $frm_vars;
1376 return self::is_option_true( $field, 'read_only' ) && ( ! isset( $frm_vars['readonly'] ) || $frm_vars['readonly'] !== 'disabled' );
1377 }
1378
1379 /**
1380 * @since 2.0.9
1381 *
1382 * @param array $field
1383 *
1384 * @return bool
1385 */
1386 public static function is_required( $field ) {
1387 $required = $field['required'] != '0';
1388
1389 /**
1390 * @param bool $required
1391 * @param array $field
1392 */
1393 $required = (bool) apply_filters( 'frm_is_field_required', $required, $field );
1394
1395 return $required;
1396 }
1397
1398 /**
1399 * @since 2.0.9
1400 *
1401 * @param array|object $field
1402 * @param string $option
1403 *
1404 * @return bool
1405 */
1406 public static function is_option_true( $field, $option ) {
1407 if ( is_array( $field ) ) {
1408 return self::is_option_true_in_array( $field, $option );
1409 }
1410 return self::is_option_true_in_object( $field, $option );
1411 }
1412
1413 /**
1414 * @since 2.0.9
1415 *
1416 * @param array|object $field
1417 * @param string $option
1418 *
1419 * @return bool
1420 */
1421 public static function is_option_empty( $field, $option ) {
1422 if ( is_array( $field ) ) {
1423 return self::is_option_empty_in_array( $field, $option );
1424 }
1425 return self::is_option_empty_in_object( $field, $option );
1426 }
1427
1428 /**
1429 * @param array $field
1430 * @param string $option
1431 *
1432 * @return bool
1433 */
1434 public static function is_option_true_in_array( $field, $option ) {
1435 return ! empty( $field[ $option ] );
1436 }
1437
1438 /**
1439 * @param object $field
1440 * @param string $option
1441 *
1442 * @return bool
1443 */
1444 public static function is_option_true_in_object( $field, $option ) {
1445 return isset( $field->field_options[ $option ] ) && $field->field_options[ $option ];
1446 }
1447
1448 /**
1449 * @param array $field
1450 * @param string $option
1451 *
1452 * @return bool
1453 */
1454 public static function is_option_empty_in_array( $field, $option ) {
1455 return empty( $field[ $option ] );
1456 }
1457
1458 /**
1459 * @param object $field
1460 * @param string $option
1461 *
1462 * @return bool
1463 */
1464 public static function is_option_empty_in_object( $field, $option ) {
1465 return empty( $field->field_options[ $option ] );
1466 }
1467
1468 /**
1469 * @param stdClass $field
1470 * @param string $option
1471 *
1472 * @return bool
1473 */
1474 public static function is_option_value_in_object( $field, $option ) {
1475 return isset( $field->field_options[ $option ] ) && $field->field_options[ $option ] != '';
1476 }
1477
1478 /**
1479 * @since 2.0.18
1480 *
1481 * @param array|object $field
1482 * @param string $option
1483 *
1484 * @return mixed
1485 */
1486 public static function get_option( $field, $option ) {
1487 if ( is_array( $field ) ) {
1488 $option = self::get_option_in_array( $field, $option );
1489 } else {
1490 $option = self::get_option_in_object( $field, $option );
1491 }
1492
1493 return $option;
1494 }
1495
1496 /**
1497 * @param array $field
1498 * @param string $option
1499 *
1500 * @return mixed
1501 */
1502 public static function get_option_in_array( $field, $option ) {
1503 if ( isset( $field[ $option ] ) ) {
1504 $this_option = $field[ $option ];
1505 } elseif ( isset( $field['field_options'] ) && is_array( $field['field_options'] ) && isset( $field['field_options'][ $option ] ) ) {
1506 $this_option = $field['field_options'][ $option ];
1507 } else {
1508 $this_option = '';
1509 }
1510
1511 return $this_option;
1512 }
1513
1514 /**
1515 * @param object $field
1516 * @param string $option
1517 *
1518 * @return mixed
1519 */
1520 public static function get_option_in_object( $field, $option ) {
1521 return $field->field_options[ $option ] ?? '';
1522 }
1523
1524 /**
1525 * @since 2.0.09
1526 *
1527 * @param array|object $field
1528 *
1529 * @return bool
1530 */
1531 public static function is_repeating_field( $field ) {
1532 if ( is_array( $field ) ) {
1533 $is_repeating_field = ( 'divider' === $field['type'] );
1534 } else {
1535 $is_repeating_field = ( 'divider' === $field->type );
1536 }
1537
1538 return $is_repeating_field && self::is_option_true( $field, 'repeat' );
1539 }
1540
1541 /**
1542 * @param string $key
1543 *
1544 * @return int field id
1545 */
1546 public static function get_id_by_key( $key ) {
1547 $id = FrmDb::get_var( 'frm_fields', array( 'field_key' => sanitize_title( $key ) ) );
1548
1549 return (int) $id;
1550 }
1551
1552 /**
1553 * @param string $id
1554 *
1555 * @return string|null
1556 */
1557 public static function get_key_by_id( $id ) {
1558 return FrmDb::get_var( 'frm_fields', array( 'id' => $id ), 'field_key' );
1559 }
1560
1561 /**
1562 * @param array|object $field
1563 *
1564 * @return bool
1565 */
1566 public static function is_image( $field ) {
1567 $type = self::get_field_type( $field );
1568
1569 return ( $type === 'url' && self::get_option( $field, 'show_image' ) );
1570 }
1571
1572 /**
1573 * Check if field is radio or Dynamic radio
1574 *
1575 * @since 3.0
1576 *
1577 * @param array|object $field
1578 *
1579 * @return bool true if field type is radio or Dynamic radio
1580 */
1581 public static function is_radio( $field ) {
1582 return self::is_field_type( $field, 'radio' );
1583 }
1584
1585 /**
1586 * Check if field is checkbox or Dynamic checkbox
1587 *
1588 * @since 3.0
1589 *
1590 * @param array|object $field
1591 *
1592 * @return bool true if field type is checkbox or Dynamic checkbox
1593 */
1594 public static function is_checkbox( $field ) {
1595 return self::is_field_type( $field, 'checkbox' );
1596 }
1597
1598 /**
1599 * Check if field is checkbox or radio
1600 *
1601 * @since 3.0
1602 *
1603 * @param array|object $field
1604 * @param string $is_type Options include radio, checkbox, text.
1605 *
1606 * @return bool true if field type is checkbox or Dynamic checkbox
1607 */
1608 public static function is_field_type( $field, $is_type ) {
1609 $field_type = self::get_original_field_type( $field );
1610 $data_type = self::get_option( $field, 'data_type' );
1611
1612 $is_field_type = (
1613 $is_type === $field_type ||
1614 ( 'data' === $field_type && $is_type === $data_type ) ||
1615 ( 'lookup' === $field_type && $is_type === $data_type ) ||
1616 ( 'product' === $field_type && $is_type === $data_type )
1617 );
1618
1619 /**
1620 * When a field type is checked, allow individual fields
1621 * to set the type.
1622 *
1623 * @since 4.04
1624 */
1625 return apply_filters( 'frm_is_field_type', $is_field_type, compact( 'field', 'is_type' ) );
1626 }
1627
1628 /**
1629 * Checks if the given field array is a combo field.
1630 *
1631 * @since 4.10.02
1632 *
1633 * @param array $field Field array.
1634 *
1635 * @return bool
1636 */
1637 public static function is_combo_field( $field ) {
1638 $field_type_obj = FrmFieldFactory::get_field_factory( $field );
1639
1640 return ! empty( $field_type_obj->is_combo_field );
1641 }
1642 }
1643