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

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