PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.34
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.34
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 6.5.4 All 140 releases
formidable / classes / models / FrmField.php

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

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