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

1,618 lines 50.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 class FrmField {
7
8 /**
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' => 'frmfont frm_text2_icon',
26 ),
27 'textarea' => array(
28 'name' => __( 'Paragraph', 'formidable' ),
29 'icon' => 'frmfont frm_paragraph_icon',
30 ),
31 'checkbox' => array(
32 'name' => __( 'Checkboxes', 'formidable' ),
33 'icon' => 'frmfont frm_check_square_icon',
34 ),
35 'radio' => array(
36 'name' => __( 'Radio Buttons', 'formidable' ),
37 'icon' => 'frmfont frm_radio_checked_icon',
38 ),
39 'select' => array(
40 'name' => __( 'Dropdown', 'formidable' ),
41 'icon' => 'frmfont frm_dropdown_icon',
42 ),
43 'email' => array(
44 'name' => __( 'Email', 'formidable' ),
45 'icon' => 'frmfont frm_email2_icon',
46 ),
47 'url' => array(
48 'name' => __( 'Website/URL', 'formidable' ),
49 'icon' => 'frmfont frm_link2_icon',
50 ),
51 'number' => array(
52 'name' => __( 'Number', 'formidable' ),
53 'icon' => 'frmfont frm_hashtag_icon',
54 ),
55 'name' => array(
56 'name' => __( 'Name', 'formidable' ),
57 'icon' => 'frmfont frm_user_name_icon',
58 ),
59 'phone' => array(
60 'name' => __( 'Phone', 'formidable' ),
61 'icon' => 'frmfont frm_phone_icon',
62 ),
63 'html' => array(
64 'name' => __( 'HTML', 'formidable' ),
65 'icon' => 'frmfont frm_code2_icon',
66 ),
67 'hidden' => array(
68 'name' => __( 'Hidden', 'formidable' ),
69 'icon' => 'frmfont frm_eye_slash2_icon',
70 ),
71 'user_id' => array(
72 'name' => __( 'User ID', 'formidable' ),
73 'icon' => 'frmfont frm_user2_icon',
74 ),
75 'captcha' => array(
76 'name' => self::get_captcha_field_name(),
77 'icon' => 'frmfont frm_shield_check2_icon',
78 ),
79 'credit_card' => array(
80 'name' => __( 'Payment', 'formidable' ),
81 'icon' => 'frmfont 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' => 'frmfont 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' => 'frmfont 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' => 'frmfont 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' => 'frmfont 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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
141 'upsell_image' => $upsell_images_url . 'date-field-preview.webp',
142 'learn-more' => 'date',
143 ),
144 'time' => array(
145 'name' => __( 'Time', 'formidable' ),
146 'icon' => 'frmfont frm_clock_icon',
147 'message' => __( 'Precisely log important moments with an intuitive time selection tool, perfect for managing events, availability, or operational hours.', 'formidable' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
148 'upsell_image' => $upsell_images_url . 'time-field-preview.webp',
149 'learn-more' => 'time',
150 ),
151 'scale' => array(
152 'name' => __( 'Scale', 'formidable' ),
153 'icon' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
169 'upsell_image' => $upsell_images_url . 'slider-field-preview.webp',
170 'learn-more' => 'slider',
171 ),
172 'toggle' => array(
173 'name' => __( 'Toggle', 'formidable' ),
174 'icon' => 'frmfont 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' => 'frmfont 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' => 'frmfont 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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
265 'upsell_image' => $upsell_images_url . 'address-field-preview.webp',
266 'learn-more' => 'address',
267 ),
268 'summary' => array(
269 'name' => __( 'Summary', 'formidable' ),
270 'icon' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
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' => 'frmfont 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' => 'frmfont 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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
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' => 'frmfont 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' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
326 'upsell_image' => $upsell_images_url . 'total-field-preview.webp',
327 'learn-more' => 'features/pricing-fields',
328 ),
329 'coupon' => array(
330 'name' => __( 'Coupon', 'formidable' ),
331 'icon' => 'frm_icon_font frm_coupon_icon frm_show_upgrade',
332 'addon' => 'coupons',
333 'section' => 'pricing',
334 'limit' => 1,
335 'is_new' => self::field_is_new( 'coupon' ),
336 ),
337 );
338
339 // Since the signature field may be in a different section, don't show it twice.
340 $lite_fields = self::field_selection();
341
342 if ( isset( $lite_fields['signature'] ) ) {
343 unset( $fields['signature'] );
344 }
345
346 return apply_filters( 'frm_pro_available_fields', $fields );
347 }
348
349 /**
350 * Consider a field new for 90 days after the release date.
351 *
352 * @since 6.8.3
353 *
354 * @param string $type
355 *
356 * @return bool
357 */
358 private static function field_is_new( $type ) {
359 $release_dates = array(
360 'coupon' => '2026-01-13',
361 );
362
363 if ( ! isset( $release_dates[ $type ] ) ) {
364 return false;
365 }
366
367 $release_date = $release_dates[ $type ];
368 $three_months_after_release = gmdate( 'Y-m-d', strtotime( $release_date . ' + 90 days' ) );
369 return gmdate( 'Y-m-d' ) < $three_months_after_release;
370 }
371
372 /**
373 * @since 4.0
374 *
375 * @return array
376 */
377 public static function all_field_selection() {
378 $pro_field_selection = self::pro_field_selection();
379 return array_merge( $pro_field_selection, self::field_selection() );
380 }
381
382 /**
383 * Create a field.
384 *
385 * @param array $values
386 * @param bool $return
387 *
388 * @return false|int
389 */
390 public static function create( $values, $return = true ) {
391 global $wpdb, $frm_duplicate_ids;
392
393 $new_values = array();
394 $key = $values['field_key'] ?? $values['name'];
395 $new_values['field_key'] = FrmAppHelper::get_unique_key( $key, $wpdb->prefix . 'frm_fields', 'field_key' );
396
397 $values = FrmAppHelper::maybe_filter_array( $values, array( 'name', 'description' ) );
398
399 foreach ( array( 'name', 'description', 'type', 'default_value' ) as $col ) {
400 if ( isset( $values[ $col ] ) ) {
401 $new_values[ $col ] = $values[ $col ];
402 }
403 }
404
405 $new_values['options'] = self::maybe_filter_options( $values['options'] );
406 $new_values['field_order'] = isset( $values['field_order'] ) ? (int) $values['field_order'] : null;
407 $new_values['required'] = isset( $values['required'] ) ? (int) $values['required'] : 0;
408 $new_values['form_id'] = isset( $values['form_id'] ) ? (int) $values['form_id'] : null;
409 $new_values['field_options'] = $values['field_options'];
410 $new_values['created_at'] = current_time( 'mysql', 1 );
411
412 if ( isset( $values['id'] ) ) {
413 $frm_duplicate_ids[ $values['field_key'] ] = $new_values['field_key'];
414 $new_values = apply_filters( 'frm_duplicated_field', $new_values );
415 }
416
417 self::preserve_format_option_backslashes( $new_values );
418
419 foreach ( $new_values as $k => $v ) {
420 if ( is_array( $v ) ) {
421 $new_values[ $k ] = $k === 'default_value' ? FrmAppHelper::maybe_json_encode( $v ) : serialize( $v );
422 }
423 unset( $k, $v );
424 }
425
426 $query_results = $wpdb->insert( $wpdb->prefix . 'frm_fields', $new_values );
427
428 if ( ! $query_results ) {
429 return false;
430 }
431
432 self::delete_form_transient( $new_values['form_id'] );
433
434 if ( ! $return ) {
435 return false;
436 }
437
438 $new_id = $wpdb->insert_id;
439
440 if ( isset( $values['id'] ) ) {
441 $frm_duplicate_ids[ $values['id'] ] = $new_id;
442 }
443
444 return $new_id;
445 }
446
447 /**
448 * Maybe filter HTML in field options data.
449 * HTML is only filtered when unsafe HTML is disallowed.
450 * See FrmAppHelper::allow_unfiltered_html.
451 *
452 * @since 5.0.08
453 *
454 * @param array $options
455 *
456 * @return array
457 */
458 private static function maybe_filter_options( $options ) {
459 $options = FrmAppHelper::maybe_filter_array( $options, array( 'custom_html' ) );
460
461 if ( ! empty( $options['custom_html'] ) ) {
462 $options['custom_html'] = self::maybe_filter_custom_html_input_attributes( $options['custom_html'] );
463 }
464
465 if ( ! empty( $options['classes'] ) ) {
466 $options['classes'] = implode( ' ', array_map( 'FrmFormsHelper::sanitize_layout_class', explode( ' ', $options['classes'] ) ) );
467 }
468
469 return $options;
470 }
471
472 /**
473 * Prevent users who do not have permission to insert JavaScript attributes in input elements.
474 * This is triggered when a field is updated.
475 *
476 * @since 6.11.2
477 *
478 * @param string $html
479 *
480 * @return string
481 */
482 private static function maybe_filter_custom_html_input_attributes( $html ) {
483 if ( FrmAppHelper::allow_unfiltered_html() ) {
484 return $html;
485 }
486
487 $pattern = get_shortcode_regex( array( 'input' ) );
488 return preg_replace_callback(
489 "/$pattern/",
490 /**
491 * @param array $match Shortcode data.
492 *
493 * @return string
494 */
495 function ( $match ) {
496 $attr = shortcode_parse_atts( $match[3] );
497
498 if ( ! is_array( $attr ) ) {
499 // In old versions of WordPress (older than 6.5), this might not be an array.
500 return '[input]';
501 }
502
503 $safe_atts = array();
504
505 foreach ( $attr as $attr_key => $att ) {
506 if ( ! is_numeric( $attr_key ) ) {
507 // opt=1 without parentheses for example is mapped like 'opt' => 1.
508 $key = $attr_key;
509 $value = $att;
510 } else {
511 // Some data is mapped like 0 => 'placeholder="Placeholder"'.
512 $split = explode( '=', $att, 2 );
513
514 if ( 2 !== count( $split ) ) {
515 continue;
516 }
517
518 $key = trim( $split[0] );
519 $value = trim( $split[1], '"' );
520 }
521
522 if ( FrmAppHelper::input_key_is_safe( $key, 'update' ) ) {
523 $safe_atts[ $key ] = $value;
524 }
525 }//end foreach
526
527 if ( ! $safe_atts ) {
528 return '[input]';
529 }
530
531 return '[input ' . FrmAppHelper::array_to_html_params( $safe_atts ) . ']';
532 },
533 $html
534 );
535 }
536
537 /**
538 * Process the field duplication.
539 *
540 * @since 5.0.05
541 *
542 * @param int|string $field_id Field ID.
543 * @param int|string $form_id Form ID.
544 *
545 * @return array|false
546 */
547 public static function duplicate_single_field( $field_id, $form_id ) {
548 $copy_field = self::getOne( $field_id );
549
550 if ( ! $copy_field ) {
551 return false;
552 }
553
554 do_action( 'frm_duplicate_field', $copy_field, $form_id );
555 do_action( 'frm_duplicate_field_' . $copy_field->type, $copy_field, $form_id );
556
557 $values = array(
558 'id' => $copy_field->id,
559 );
560 FrmFieldsHelper::fill_field( $values, $copy_field, $copy_field->form_id );
561 $values = apply_filters( 'frm_prepare_single_field_for_duplication', $values );
562 $field_id = self::create( $values );
563
564 /**
565 * Fires after duplicating a field.
566 *
567 * @since 5.0.04
568 *
569 * @param array $args {
570 * The arguments.
571 *
572 * @type int $field_id New field ID.
573 * @type array $values Values before inserting.
574 * @type object $copy_field Copy field data.
575 * @type int $form_id Form ID.
576 * }
577 */
578 do_action( 'frm_after_duplicate_field', compact( 'field_id', 'values', 'copy_field', 'form_id' ) );
579
580 return compact( 'field_id', 'values' );
581 }
582
583 /**
584 * @param int|string $old_form_id
585 * @param int|string $form_id
586 * @param bool $copy_keys
587 * @param false|int $blog_id
588 *
589 * @return void
590 */
591 public static function duplicate( $old_form_id, $form_id, $copy_keys = false, $blog_id = false ) {
592 global $frm_duplicate_ids;
593
594 $where = array(
595 array(
596 'or' => 1,
597 'fi.form_id' => $old_form_id,
598 'fr.parent_form_id' => $old_form_id,
599 ),
600 );
601 $fields = self::getAll( $where, 'field_order', '', $blog_id );
602
603 foreach ( (array) $fields as $field ) {
604 $new_key = $copy_keys ? $field->field_key : '';
605
606 // phpcs:ignore Universal.Operators.StrictComparisons
607 if ( $copy_keys && substr( $field->field_key, - 1 ) == 2 ) {
608 $new_key = rtrim( $new_key, 2 );
609 }
610
611 $values = array();
612 FrmFieldsHelper::fill_field( $values, $field, $form_id, $new_key );
613
614 // If this is a repeating section, create new form
615 if ( self::is_repeating_field( $field ) ) {
616 // Create the repeatable form
617 $new_repeat_form_id = apply_filters(
618 'frm_create_repeat_form',
619 0,
620 array(
621 'parent_form_id' => $form_id,
622 'field_name' => $field->name,
623 )
624 );
625
626 // Save old form_select
627 $old_repeat_form_id = $field->field_options['form_select'];
628
629 // Update form_select for repeating field
630 $values['field_options']['form_select'] = $new_repeat_form_id;
631 }
632
633 // If this is a field inside of a repeating section, associate it with the correct form
634 if ( (int) $field->form_id !== (int) $old_form_id && isset( $old_repeat_form_id ) && isset( $new_repeat_form_id ) && (int) $field->form_id === (int) $old_repeat_form_id ) { // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
635 $values['form_id'] = $new_repeat_form_id;
636 }
637
638 $values['description'] = FrmFieldsHelper::switch_field_ids( $values['description'] );
639
640 $values = apply_filters( 'frm_duplicated_field', $values );
641 $new_id = self::create( $values );
642 $frm_duplicate_ids[ $field->id ] = $new_id;
643 $frm_duplicate_ids[ $field->field_key ] = $new_id;
644 unset( $field );
645 }//end foreach
646 }
647
648 /**
649 * @param int|string $id
650 * @param array $values
651 *
652 * @return false|int
653 */
654 public static function update( $id, $values ) {
655 global $wpdb;
656
657 $id = absint( $id );
658 $values = FrmAppHelper::maybe_filter_array( $values, array( 'name', 'description' ) );
659
660 if ( isset( $values['field_key'] ) ) {
661 $values['field_key'] = FrmAppHelper::get_unique_key( $values['field_key'], $wpdb->prefix . 'frm_fields', 'field_key', $id );
662 }
663
664 if ( isset( $values['required'] ) ) {
665 $values['required'] = (int) $values['required'];
666 }
667
668 self::preserve_format_option_backslashes( $values );
669
670 if ( isset( $values['type'] ) ) {
671 if ( 'dropdown' === $values['type'] ) {
672 // To avoid conflicts with security plugins the value "dropdown" is sent for select fields.
673 // This is because "select" gets matched for SQL injection attempts.
674 $values['type'] = 'select';
675 }
676
677 /**
678 * @since 6.9 The Field ID param was added.
679 *
680 * @param array $values
681 * @param int $id Field ID.
682 */
683 $values = apply_filters( 'frm_clean_' . $values['type'] . '_field_options_before_update', $values, $id );
684
685 if ( $values['type'] === 'hidden' && isset( $values['field_options'] ) && isset( $values['field_options']['clear_on_focus'] ) ) {
686 // Don't keep the old placeholder setting for hidden fields
687 $values['field_options']['clear_on_focus'] = 0;
688 }
689 }
690
691 // Serialize array values
692 foreach ( array( 'field_options', 'options' ) as $opt ) {
693 if ( isset( $values[ $opt ] ) && is_array( $values[ $opt ] ) ) {
694 if ( 'field_options' === $opt ) {
695 $values[ $opt ] = self::maybe_filter_options( $values[ $opt ] );
696 }
697
698 $values[ $opt ] = serialize( $values[ $opt ] );
699 }
700 }
701
702 if ( isset( $values['default_value'] ) && is_array( $values['default_value'] ) ) {
703 $values['default_value'] = json_encode( $values['default_value'] );
704 }
705
706 $query_results = $wpdb->update( $wpdb->prefix . 'frm_fields', $values, array( 'id' => $id ) );
707 $form_id = 0;
708
709 if ( isset( $values['form_id'] ) ) {
710 $form_id = absint( $values['form_id'] );
711 } else {
712 $field = self::getOne( $id );
713
714 if ( $field ) {
715 $form_id = $field->form_id;
716 }
717 unset( $field );
718 }
719 unset( $values );
720
721 if ( $query_results ) {
722 wp_cache_delete( $id, 'frm_field' );
723
724 if ( $form_id ) {
725 self::delete_form_transient( $form_id );
726 }
727 }
728
729 return $query_results;
730 }
731
732 /**
733 * Keep backslashes in the phone format option
734 *
735 * @since 2.0.8
736 *
737 * @param array $values Pass by reference.
738 *
739 * @return void
740 */
741 private static function preserve_format_option_backslashes( &$values ) {
742 if ( isset( $values['field_options']['format'] ) ) {
743 $values['field_options']['format'] = FrmAppHelper::preserve_backslashes( $values['field_options']['format'] );
744 }
745 }
746
747 /**
748 * @param int|string $id
749 *
750 * @return bool|int
751 */
752 public static function destroy( $id ) {
753 global $wpdb;
754
755 do_action( 'frm_before_destroy_field', $id );
756
757 wp_cache_delete( $id, 'frm_field' );
758 $field = self::getOne( $id );
759
760 if ( ! $field ) {
761 return false;
762 }
763
764 self::delete_form_transient( $field->form_id );
765
766 $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_item_metas WHERE field_id=%d', $id ) );
767
768 return $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_fields WHERE id=%d', $id ) );
769 }
770
771 /**
772 * @param int|string $form_id
773 *
774 * @return void
775 */
776 public static function delete_form_transient( $form_id ) {
777 $form_id = absint( $form_id );
778 delete_transient( 'frm_form_fields_' . $form_id . 'excludeinclude' );
779 delete_transient( 'frm_form_fields_' . $form_id . 'includeinclude' );
780 delete_transient( 'frm_form_fields_' . $form_id . 'includeexclude' );
781 delete_transient( 'frm_form_fields_' . $form_id . 'excludeexclude' );
782
783 global $wpdb;
784 $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%' ) ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
785
786 FrmDb::cache_delete_group( 'frm_field' );
787
788 $form = FrmForm::getOne( $form_id );
789
790 if ( $form && $form->parent_form_id && (int) $form->parent_form_id !== $form_id ) {
791 self::delete_form_transient( $form->parent_form_id );
792 }
793 }
794
795 /**
796 * If $field is numeric, get the field object
797 *
798 * @param int|object|string $field
799 *
800 * @return void
801 */
802 public static function maybe_get_field( &$field ) {
803 if ( ! is_object( $field ) ) {
804 $field = self::getOne( $field );
805 }
806 }
807
808 /**
809 * @param int|string $id The field id or key.
810 * @param bool $filter When true, run the frm_field filter.
811 *
812 * @return object|null
813 */
814 public static function getOne( $id, $filter = false ) {
815 if ( ! $id ) {
816 return null;
817 }
818
819 global $wpdb;
820
821 $where = is_numeric( $id ) ? 'id=%d' : 'field_key=%s';
822 $query = $wpdb->prepare( 'SELECT * FROM ' . $wpdb->prefix . 'frm_fields WHERE ' . $where, $id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
823
824 $results = FrmDb::check_cache( $id, 'frm_field', $query, 'get_row', 0 );
825
826 if ( ! $results ) {
827 self::filter_field( $filter, $results );
828 return $results;
829 }
830
831 if ( is_numeric( $id ) ) {
832 FrmDb::set_cache( $results->field_key, $results, 'frm_field' );
833 } elseif ( $results ) {
834 FrmDb::set_cache( $results->id, $results, 'frm_field' );
835 }
836
837 self::prepare_options( $results );
838 self::filter_field( $filter, $results );
839
840 return wp_unslash( $results );
841 }
842
843 /**
844 * @since 3.06.01
845 *
846 * @param bool $filter When true, run the frm_field filter.
847 * @param object $results
848 *
849 * @return void
850 */
851 private static function filter_field( $filter, &$results ) {
852 if ( $filter ) {
853 /**
854 * @since 3.06.01
855 */
856 $results = apply_filters( 'frm_field', $results );
857 }
858 }
859
860 /**
861 * Get the field type by key or id
862 *
863 * @param int|string $id The field id or key.
864 * @param mixed $col The name of the column in the fields database table.
865 *
866 * @return mixed
867 */
868 public static function get_type( $id, $col = 'type' ) {
869 $field = FrmDb::check_cache( $id, 'frm_field' );
870
871 if ( $field ) {
872 return $field->{$col};
873 }
874
875 $where = is_numeric( $id ) ? array( 'id' => $id ) : array( 'field_key' => $id );
876
877 return FrmDb::get_var( 'frm_fields', $where, $col );
878 }
879
880 /**
881 * @param int|string $form_id
882 * @param string $type
883 * @param int|string $limit
884 * @param string $inc_sub
885 *
886 * @return array|object
887 */
888 public static function get_all_types_in_form( $form_id, $type, $limit = '', $inc_sub = 'exclude' ) {
889 if ( ! $form_id ) {
890 return array();
891 }
892
893 $results = self::get_fields_from_transients(
894 $form_id,
895 array(
896 'inc_embed' => $inc_sub,
897 'inc_repeat' => $inc_sub,
898 )
899 );
900
901 if ( $results ) {
902 $fields = array();
903 $count = 0;
904
905 foreach ( $results as $result ) {
906 if ( $type !== $result->type ) {
907 continue;
908 }
909
910 $fields[ $result->id ] = $result;
911 ++$count;
912
913 // phpcs:ignore Universal.Operators.StrictComparisons
914 if ( $limit == 1 ) {
915 $fields = $result;
916 break;
917 }
918
919 if ( $limit && $count >= $limit ) {
920 break;
921 }
922
923 unset( $result );
924 }
925
926 return wp_unslash( $fields );
927 }//end if
928
929 self::$use_cache = false;
930
931 $where = array(
932 'fi.form_id' => (int) $form_id,
933 'fi.type' => $type,
934 );
935 self::maybe_include_repeating_fields( $inc_sub, $where );
936 $results = self::getAll( $where, 'field_order', $limit );
937 self::$use_cache = true;
938 self::include_sub_fields( $results, $inc_sub, $type, $form_id );
939
940 return $results;
941 }
942
943 /**
944 * @param int|string $form_id
945 * @param int|string $limit
946 * @param string $inc_embed
947 * @param string $inc_repeat
948 *
949 * @return array
950 */
951 public static function get_all_for_form( $form_id, $limit = '', $inc_embed = 'exclude', $inc_repeat = 'include' ) {
952 if ( ! (int) $form_id ) {
953 return array();
954 }
955
956 $results = self::get_fields_from_transients( $form_id, compact( 'inc_embed', 'inc_repeat' ) );
957
958 if ( $results ) {
959 if ( ! $limit ) {
960 return $results;
961 }
962
963 $fields = array();
964 $count = 0;
965
966 foreach ( $results as $result ) {
967 ++$count;
968 $fields[ $result->id ] = $result;
969
970 if ( $limit && $count >= $limit ) {
971 break;
972 }
973 }
974
975 return $fields;
976 }
977
978 self::$use_cache = false;
979
980 $where = array( 'fi.form_id' => absint( $form_id ) );
981 self::maybe_include_repeating_fields( $inc_repeat, $where );
982 $results = self::getAll( $where, 'field_order', $limit );
983
984 self::$use_cache = true;
985
986 self::include_sub_fields( $results, $inc_embed, 'all', $form_id );
987
988 if ( ! $limit ) {
989 self::set_field_transient( $results, $form_id, 0, compact( 'inc_embed', 'inc_repeat' ) );
990 }
991
992 return $results;
993 }
994
995 /**
996 * If repeating fields should be included, adjust $where accordingly
997 *
998 * @param string $inc_repeat
999 * @param array $where Pass by reference.
1000 *
1001 * @return void
1002 */
1003 private static function maybe_include_repeating_fields( $inc_repeat, &$where ) {
1004 if ( $inc_repeat === 'include' ) {
1005 $form_id = $where['fi.form_id'];
1006 $where[] = array(
1007 'or' => 1,
1008 'fi.form_id' => $form_id,
1009 'fr.parent_form_id' => $form_id,
1010 );
1011 unset( $where['fi.form_id'] );
1012 }
1013 }
1014
1015 /**
1016 * @param array $results
1017 * @param string $inc_embed
1018 * @param string $type
1019 * @param int|string $form_id
1020 *
1021 * @return void
1022 */
1023 public static function include_sub_fields( &$results, $inc_embed, $type = 'all', $form_id = '' ) {
1024 $no_sub_forms = ! $results && $type === 'all';
1025
1026 if ( 'include' !== $inc_embed || $no_sub_forms ) {
1027 return;
1028 }
1029
1030 $form_fields = $results;
1031 $should_get_subforms = $type !== 'all' && $type !== 'form' && $form_id;
1032
1033 if ( $should_get_subforms ) {
1034 $form_fields = self::get_all_types_in_form( $form_id, 'form' );
1035 }
1036
1037 $index_offset = 1;
1038
1039 foreach ( $form_fields as $k => $field ) {
1040 if ( 'form' !== $field->type || ! isset( $field->field_options['form_select'] ) ) {
1041 continue;
1042 }
1043
1044 if ( $type === 'all' ) {
1045 $sub_fields = self::get_all_for_form( $field->field_options['form_select'] );
1046 } else {
1047 $sub_fields = self::get_all_types_in_form( $field->field_options['form_select'], $type );
1048 }
1049
1050 if ( ! empty( $sub_fields ) ) {
1051 $index = $k + $index_offset;
1052 $index_offset += count( $sub_fields );
1053 array_splice( $results, $index, 0, $sub_fields );
1054 }
1055 unset( $field, $sub_fields );
1056 }
1057 }
1058
1059 /**
1060 * @param array|string $where
1061 * @param string $order_by
1062 * @param string $limit
1063 * @param false|int $blog_id
1064 *
1065 * @return array|object|null
1066 */
1067 public static function getAll( $where = array(), $order_by = '', $limit = '', $blog_id = false ) {
1068 $cache_key = FrmAppHelper::maybe_json_encode( $where ) . $order_by . 'l' . $limit . 'b' . $blog_id;
1069
1070 if ( self::$use_cache ) {
1071 // Make sure old cache doesn't get saved as a transient
1072 $results = wp_cache_get( $cache_key, 'frm_field' );
1073
1074 if ( false !== $results ) {
1075 return wp_unslash( $results );
1076 }
1077 }
1078
1079 global $wpdb;
1080
1081 if ( $blog_id && is_multisite() ) {
1082 $prefix = $wpdb->get_blog_prefix( $blog_id );
1083 $table_name = $prefix . 'frm_fields';
1084 $form_table_name = $prefix . 'frm_forms';
1085 } else {
1086 $table_name = $wpdb->prefix . 'frm_fields';
1087 $form_table_name = $wpdb->prefix . 'frm_forms';
1088 }
1089
1090 if ( $order_by && ! str_contains( $order_by, 'ORDER BY' ) ) {
1091 $order_by = ' ORDER BY ' . $order_by;
1092 }
1093
1094 $limit = FrmDb::esc_limit( $limit );
1095 $query = "SELECT fi.*, fr.name as form_name FROM {$table_name} fi JOIN {$form_table_name} fr ON fi.form_id=fr.id";
1096 $query_type = $limit === ' LIMIT 1' || $limit == 1 ? 'row' : 'results'; // phpcs:ignore Universal.Operators.StrictComparisons
1097
1098 if ( is_array( $where ) ) {
1099 $args = array(
1100 'order_by' => $order_by,
1101 'limit' => $limit,
1102 );
1103 $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 );
1104 } else {
1105 // If the query is not an array, then it has already been prepared
1106 $query .= FrmDb::prepend_and_or_where( ' WHERE ', $where ) . $order_by . $limit;
1107
1108 $function_name = $query_type === 'row' ? 'get_row' : 'get_results';
1109 $results = $wpdb->$function_name( $query );
1110 }
1111 unset( $where );
1112
1113 self::format_field_results( $results );
1114
1115 FrmDb::set_cache( $cache_key, $results, 'frm_field' );
1116
1117 return wp_unslash( $results );
1118 }
1119
1120 /**
1121 * @since 2.0.8
1122 *
1123 * @param array|object|null $results Results.
1124 *
1125 * @return void
1126 */
1127 private static function format_field_results( &$results ) {
1128 if ( is_array( $results ) ) {
1129 foreach ( $results as $r_key => $result ) {
1130 self::add_slashes_to_format_before_setting_field_cache( $result );
1131
1132 FrmDb::set_cache( $result->id, $result, 'frm_field' );
1133 FrmDb::set_cache( $result->field_key, $result, 'frm_field' );
1134
1135 self::prepare_options( $result );
1136 $results[ $r_key ]->field_options = $result->field_options;
1137 $results[ $r_key ]->options = $result->options;
1138 $results[ $r_key ]->default_value = $result->default_value;
1139
1140 unset( $r_key, $result );
1141 }
1142 } elseif ( $results ) {
1143 FrmDb::set_cache( $results->id, $results, 'frm_field' );
1144 FrmDb::set_cache( $results->field_key, $results, 'frm_field' );
1145
1146 self::prepare_options( $results );
1147 }
1148 }
1149
1150 /**
1151 * When $result->field_options is an array and not a serialized string there is only a single backslash.
1152 * Cached results are unslashed in FrmField::getAll, so we need to make sure that the cached object has an extra backslash.
1153 * Otherwise the backslash is stripped away on load.
1154 *
1155 * @since 6.15
1156 *
1157 * @param stdClass $result
1158 *
1159 * @return void
1160 */
1161 private static function add_slashes_to_format_before_setting_field_cache( $result ) {
1162 if ( ! isset( $result->field_options ) || ! is_array( $result->field_options ) || empty( $result->field_options['format'] ) ) {
1163 return;
1164 }
1165
1166 $result->field_options['format'] = addslashes( $result->field_options['format'] );
1167 }
1168
1169 /**
1170 * Unserialize all the serialized field data
1171 *
1172 * @since 2.0
1173 *
1174 * @param object $results
1175 *
1176 * @return void
1177 */
1178 private static function prepare_options( &$results ) {
1179 FrmAppHelper::unserialize_or_decode( $results->field_options );
1180 FrmAppHelper::unserialize_or_decode( $results->options );
1181
1182 // Allow a single box to be checked for the default value.
1183 $before = $results->default_value;
1184
1185 $field_object = FrmFieldFactory::get_field_type( $results->type );
1186
1187 if ( ! $field_object->should_unserialize_value() ) {
1188 return;
1189 }
1190
1191 FrmAppHelper::unserialize_or_decode( $results->default_value );
1192
1193 if ( $before === $results->default_value && is_string( $before ) && str_starts_with( $before, '["' ) ) {
1194 $results->default_value = FrmAppHelper::maybe_json_decode( $results->default_value );
1195 }
1196 }
1197
1198 /**
1199 * If a form has too many fields, they won't all save into a single transient.
1200 * We'll break them into groups of 200
1201 *
1202 * @since 2.0.1
1203 *
1204 * @param int|string $form_id Form ID.
1205 * @param array $args Additional arguments.
1206 *
1207 * @return array
1208 */
1209 private static function get_fields_from_transients( $form_id, $args ) {
1210 $fields = array();
1211 self::get_next_transient( $fields, 'frm_form_fields_' . $form_id . $args['inc_embed'] . $args['inc_repeat'] );
1212
1213 return $fields;
1214 }
1215
1216 /**
1217 * Called by get_fields_from_transients
1218 *
1219 * @since 2.0.1
1220 *
1221 * @param array $fields Array of fields.
1222 * @param string $base_name Base name.
1223 * @param int $next Next transient number.
1224 *
1225 * @return void
1226 */
1227 private static function get_next_transient( &$fields, $base_name, $next = 0 ) {
1228 $name = $next ? $base_name . $next : $base_name;
1229 $next_fields = get_transient( $name );
1230
1231 if ( ! $next_fields ) {
1232 return;
1233 }
1234
1235 $fields = array_merge( $fields, $next_fields );
1236
1237 if ( count( $next_fields ) < self::$transient_size ) {
1238 return;
1239 }
1240
1241 // If this transient is full, check for another
1242 ++$next;
1243 self::get_next_transient( $fields, $base_name, $next );
1244 }
1245
1246 /**
1247 * Save the transients in chunks for large forms
1248 *
1249 * @since 2.0.1
1250 *
1251 * @param array $fields Array of fields.
1252 * @param int|string $form_id Form ID.
1253 * @param int $next Next transient number.
1254 * @param array $args Additional arguments.
1255 *
1256 * @return void
1257 */
1258 private static function set_field_transient( &$fields, $form_id, $next = 0, $args = array() ) {
1259 $base_name = 'frm_form_fields_' . $form_id . $args['inc_embed'] . $args['inc_repeat'];
1260 $field_chunks = array_chunk( $fields, self::$transient_size );
1261
1262 foreach ( $field_chunks as $field ) {
1263 $name = $next ? $base_name . $next : $base_name;
1264 $set = set_transient( $name, $field, 60 * 60 * 6 );
1265
1266 if ( ! $set ) {
1267 // The transient didn't save
1268 if ( $name !== $base_name ) {
1269 // If the first saved an others fail, this will show an incomplete form
1270 self::delete_form_transient( $form_id );
1271 }
1272
1273 return;
1274 }
1275
1276 ++$next;
1277 }
1278 }
1279
1280 /**
1281 * @param string $type
1282 *
1283 * @return bool
1284 */
1285 public static function is_no_save_field( $type ) {
1286 return in_array( $type, self::no_save_fields(), true );
1287 }
1288
1289 /**
1290 * @return string[]
1291 */
1292 public static function no_save_fields() {
1293 return array( 'divider', 'end_divider', 'captcha', 'break', 'html', 'form', 'summary', FrmSubmitHelper::FIELD_TYPE );
1294 }
1295
1296 /**
1297 * Check if this field can hold an array of values
1298 *
1299 * @since 2.0.9
1300 *
1301 * @param array|object $field
1302 *
1303 * @return bool
1304 */
1305 public static function is_field_with_multiple_values( $field ) {
1306 if ( ! $field ) {
1307 return false;
1308 }
1309
1310 $field_type = self::get_original_field_type( $field );
1311
1312 return self::is_checkbox( $field ) || $field_type === 'address' || self::is_multiple_select( $field );
1313 }
1314
1315 /**
1316 * @since 3.0
1317 *
1318 * @param array|object $field
1319 *
1320 * @return string
1321 */
1322 public static function get_field_type( $field ) {
1323 return is_array( $field ) ? $field['type'] : $field->type;
1324 }
1325
1326 /**
1327 * @since 3.0
1328 *
1329 * @param array|object $field
1330 *
1331 * @return string
1332 */
1333 public static function get_original_field_type( $field ) {
1334 $field_type = self::get_field_type( $field );
1335 $original_type = self::get_option( $field, 'original_type' );
1336
1337 if ( $original_type && $original_type !== $field_type ) {
1338 // Check the original type for arrays.
1339 return $original_type;
1340 }
1341
1342 return $field_type;
1343 }
1344
1345 /**
1346 * Check if this is a multiselect dropdown field
1347 *
1348 * @since 2.0.9
1349 *
1350 * @param array|object $field Field object.
1351 *
1352 * @return bool
1353 */
1354 public static function is_multiple_select( $field ) {
1355 $field_type = self::get_field_type( $field );
1356 $is_multiple = self::is_option_true( $field, 'multiple' ) && self::is_field_type( $field, 'select' ) && $field_type !== 'hidden';
1357
1358 return apply_filters( 'frm_is_multiple_select', $is_multiple, $field );
1359 }
1360
1361 /**
1362 * Check if a field is read only. Read only can be set in the field options,
1363 * but disabled with the shortcode options
1364 *
1365 * @since 2.0.9
1366 *
1367 * @param array|object $field
1368 *
1369 * @return bool
1370 */
1371 public static function is_read_only( $field ) {
1372 global $frm_vars;
1373 return self::is_option_true( $field, 'read_only' ) && ( ! isset( $frm_vars['readonly'] ) || $frm_vars['readonly'] !== 'disabled' );
1374 }
1375
1376 /**
1377 * @since 2.0.9
1378 *
1379 * @param array $field
1380 *
1381 * @return bool
1382 */
1383 public static function is_required( $field ) {
1384 $required = $field['required'] != '0'; // phpcs:ignore Universal.Operators.StrictComparisons
1385
1386 /**
1387 * @param bool $required
1388 * @param array $field
1389 */
1390 return (bool) apply_filters( 'frm_is_field_required', $required, $field );
1391 }
1392
1393 /**
1394 * @since 2.0.9
1395 *
1396 * @param array|object $field
1397 * @param string $option
1398 *
1399 * @return bool
1400 */
1401 public static function is_option_true( $field, $option ) {
1402 if ( is_array( $field ) ) {
1403 return self::is_option_true_in_array( $field, $option );
1404 }
1405 return self::is_option_true_in_object( $field, $option );
1406 }
1407
1408 /**
1409 * @since 2.0.9
1410 *
1411 * @param array|object $field
1412 * @param string $option
1413 *
1414 * @return bool
1415 */
1416 public static function is_option_empty( $field, $option ) {
1417 if ( is_array( $field ) ) {
1418 return self::is_option_empty_in_array( $field, $option );
1419 }
1420 return self::is_option_empty_in_object( $field, $option );
1421 }
1422
1423 /**
1424 * @param array $field
1425 * @param string $option
1426 *
1427 * @return bool
1428 */
1429 public static function is_option_true_in_array( $field, $option ) {
1430 return ! empty( $field[ $option ] );
1431 }
1432
1433 /**
1434 * @param object $field
1435 * @param string $option
1436 *
1437 * @return bool
1438 */
1439 public static function is_option_true_in_object( $field, $option ) {
1440 return ! empty( $field->field_options[ $option ] );
1441 }
1442
1443 /**
1444 * @param array $field
1445 * @param string $option
1446 *
1447 * @return bool
1448 */
1449 public static function is_option_empty_in_array( $field, $option ) {
1450 return empty( $field[ $option ] );
1451 }
1452
1453 /**
1454 * @param object $field
1455 * @param string $option
1456 *
1457 * @return bool
1458 */
1459 public static function is_option_empty_in_object( $field, $option ) {
1460 return empty( $field->field_options[ $option ] );
1461 }
1462
1463 /**
1464 * @param stdClass $field
1465 * @param string $option
1466 *
1467 * @return bool
1468 */
1469 public static function is_option_value_in_object( $field, $option ) {
1470 return isset( $field->field_options[ $option ] ) && $field->field_options[ $option ] != ''; // phpcs:ignore Universal.Operators.StrictComparisons
1471 }
1472
1473 /**
1474 * @since 2.0.18
1475 *
1476 * @param array|object $field
1477 * @param string $option
1478 *
1479 * @return mixed
1480 */
1481 public static function get_option( $field, $option ) {
1482 return is_array( $field ) ? self::get_option_in_array( $field, $option ) : self::get_option_in_object( $field, $option );
1483 }
1484
1485 /**
1486 * @param array $field
1487 * @param string $option
1488 *
1489 * @return mixed
1490 */
1491 public static function get_option_in_array( $field, $option ) {
1492 if ( isset( $field[ $option ] ) ) {
1493 $this_option = $field[ $option ];
1494 } elseif ( isset( $field['field_options'] ) && is_array( $field['field_options'] ) && isset( $field['field_options'][ $option ] ) ) {
1495 $this_option = $field['field_options'][ $option ];
1496 } else {
1497 $this_option = '';
1498 }
1499
1500 return $this_option;
1501 }
1502
1503 /**
1504 * @param object $field
1505 * @param string $option
1506 *
1507 * @return mixed
1508 */
1509 public static function get_option_in_object( $field, $option ) {
1510 return $field->field_options[ $option ] ?? '';
1511 }
1512
1513 /**
1514 * @since 2.0.09
1515 *
1516 * @param array|object $field
1517 *
1518 * @return bool
1519 */
1520 public static function is_repeating_field( $field ) {
1521 $is_repeating_field = is_array( $field ) ? 'divider' === $field['type'] : 'divider' === $field->type;
1522 return $is_repeating_field && self::is_option_true( $field, 'repeat' );
1523 }
1524
1525 /**
1526 * @param string $key
1527 *
1528 * @return int field id
1529 */
1530 public static function get_id_by_key( $key ) {
1531 $id = FrmDb::get_var( 'frm_fields', array( 'field_key' => sanitize_title( $key ) ) );
1532 return (int) $id;
1533 }
1534
1535 /**
1536 * @param string $id
1537 *
1538 * @return string|null
1539 */
1540 public static function get_key_by_id( $id ) {
1541 return FrmDb::get_var( 'frm_fields', array( 'id' => $id ), 'field_key' );
1542 }
1543
1544 /**
1545 * @param array|object $field
1546 *
1547 * @return bool
1548 */
1549 public static function is_image( $field ) {
1550 $type = self::get_field_type( $field );
1551 return $type === 'url' && self::get_option( $field, 'show_image' );
1552 }
1553
1554 /**
1555 * Check if field is radio or Dynamic radio
1556 *
1557 * @since 3.0
1558 *
1559 * @param array|object $field
1560 *
1561 * @return bool true if field type is radio or Dynamic radio
1562 */
1563 public static function is_radio( $field ) {
1564 return self::is_field_type( $field, 'radio' );
1565 }
1566
1567 /**
1568 * Check if field is checkbox or Dynamic checkbox
1569 *
1570 * @since 3.0
1571 *
1572 * @param array|object $field
1573 *
1574 * @return bool true if field type is checkbox or Dynamic checkbox
1575 */
1576 public static function is_checkbox( $field ) {
1577 return self::is_field_type( $field, 'checkbox' );
1578 }
1579
1580 /**
1581 * Check if field is checkbox or radio
1582 *
1583 * @since 3.0
1584 *
1585 * @param array|object $field
1586 * @param string $is_type Options include radio, checkbox, text.
1587 *
1588 * @return bool true if field type is checkbox or Dynamic checkbox
1589 */
1590 public static function is_field_type( $field, $is_type ) {
1591 $field_type = self::get_original_field_type( $field );
1592 $data_type = self::get_option( $field, 'data_type' );
1593 $is_field_type = $is_type === $field_type || ( 'data' === $field_type && $is_type === $data_type ) || ( 'lookup' === $field_type && $is_type === $data_type ) || ( 'product' === $field_type && $is_type === $data_type ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
1594
1595 /**
1596 * When a field type is checked, allow individual fields
1597 * to set the type.
1598 *
1599 * @since 4.04
1600 */
1601 return apply_filters( 'frm_is_field_type', $is_field_type, compact( 'field', 'is_type' ) );
1602 }
1603
1604 /**
1605 * Checks if the given field array is a combo field.
1606 *
1607 * @since 4.10.02
1608 *
1609 * @param array $field Field array.
1610 *
1611 * @return bool
1612 */
1613 public static function is_combo_field( $field ) {
1614 $field_type_obj = FrmFieldFactory::get_field_factory( $field );
1615 return ! empty( $field_type_obj->is_combo_field );
1616 }
1617 }
1618