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

1,702 lines 53.5 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 // Both option columns are stored serialized, but they do not accept the same shapes.
482 // Choice fields pass options already serialized, as does FrmFieldsHelper::fill_field
483 // when a field is duplicated, and fields with no choices pass an empty string, so a
484 // missing key is the only thing worth defaulting. An is_array check here would throw
485 // every one of those defaults away. field_options is always an array, so anything
486 // else is invalid input, and replacing it stops consumers getting null.
487 $new_values['options'] = self::maybe_filter_options( $values['options'] ?? array() );
488 $new_values['field_order'] = isset( $values['field_order'] ) ? (int) $values['field_order'] : null;
489 $new_values['required'] = isset( $values['required'] ) ? (int) $values['required'] : 0;
490 $new_values['form_id'] = isset( $values['form_id'] ) ? (int) $values['form_id'] : null;
491 $new_values['field_options'] = isset( $values['field_options'] ) && is_array( $values['field_options'] ) ? $values['field_options'] : array();
492 $new_values['created_at'] = current_time( 'mysql', 1 );
493
494 if ( isset( $values['id'] ) ) {
495 $frm_duplicate_ids[ $values['field_key'] ] = $new_values['field_key'];
496 $new_values = apply_filters( 'frm_duplicated_field', $new_values );
497 }
498
499 self::preserve_format_option_backslashes( $new_values );
500
501 foreach ( $new_values as $k => $v ) {
502 if ( is_array( $v ) ) {
503 $new_values[ $k ] = $k === 'default_value' ? FrmAppHelper::maybe_json_encode( $v ) : serialize( $v );
504 }
505 unset( $k, $v );
506 }
507
508 $query_results = $wpdb->insert( $wpdb->prefix . 'frm_fields', $new_values );
509
510 if ( ! $query_results ) {
511 return false;
512 }
513
514 self::delete_form_transient( $new_values['form_id'] );
515
516 if ( ! $return ) {
517 return false;
518 }
519
520 $new_id = $wpdb->insert_id;
521
522 if ( isset( $values['id'] ) ) {
523 $frm_duplicate_ids[ $values['id'] ] = $new_id;
524 }
525
526 return $new_id;
527 }
528
529 /**
530 * Maybe filter HTML in field options data.
531 * HTML is only filtered when unsafe HTML is disallowed.
532 * See FrmAppHelper::allow_unfiltered_html.
533 *
534 * @since 5.0.08
535 *
536 * @param array|string $options Already serialized for choice fields, and an empty string for fields with no choices.
537 *
538 * @return array
539 */
540 private static function maybe_filter_options( $options ) {
541 $options = FrmAppHelper::maybe_filter_array( $options, array( 'custom_html' ) );
542
543 if ( ! empty( $options['custom_html'] ) ) {
544 $options['custom_html'] = self::maybe_filter_custom_html_input_attributes( $options['custom_html'] );
545 }
546
547 if ( ! empty( $options['classes'] ) ) {
548 $options['classes'] = implode( ' ', array_map( 'FrmFormsHelper::sanitize_layout_class', explode( ' ', $options['classes'] ) ) );
549 }
550
551 return $options;
552 }
553
554 /**
555 * Prevent users who do not have permission to insert JavaScript attributes in input elements.
556 * This is triggered when a field is updated.
557 *
558 * @since 6.11.2
559 *
560 * @param string $html
561 *
562 * @return string
563 */
564 private static function maybe_filter_custom_html_input_attributes( $html ) {
565 if ( FrmAppHelper::allow_unfiltered_html() ) {
566 return $html;
567 }
568
569 $pattern = get_shortcode_regex( array( 'input' ) );
570 return preg_replace_callback(
571 "/$pattern/",
572 /**
573 * @param array $match Shortcode data.
574 *
575 * @return string
576 */
577 function ( $match ) {
578 $attr = shortcode_parse_atts( $match[3] );
579
580 if ( ! is_array( $attr ) ) {
581 // In old versions of WordPress (older than 6.5), this might not be an array.
582 return '[input]';
583 }
584
585 $safe_atts = array();
586
587 foreach ( $attr as $attr_key => $att ) {
588 if ( is_numeric( $attr_key ) ) {
589 // Some data is mapped like 0 => 'placeholder="Placeholder"'.
590 $split = explode( '=', $att, 2 );
591
592 if ( 2 !== count( $split ) ) {
593 continue;
594 }
595
596 $key = trim( $split[0] );
597 $value = trim( $split[1], '"' );
598 } else {
599 // opt=1 without parentheses for example is mapped like 'opt' => 1.
600 $key = $attr_key;
601 $value = $att;
602 }
603
604 if ( FrmAppHelper::input_key_is_safe( $key, 'update' ) ) {
605 $safe_atts[ $key ] = $value;
606 }
607 }//end foreach
608
609 if ( ! $safe_atts ) {
610 return '[input]';
611 }
612
613 return '[input ' . FrmAppHelper::array_to_html_params( $safe_atts ) . ']';
614 },
615 $html
616 );
617 }
618
619 /**
620 * Process the field duplication.
621 *
622 * @since 5.0.05
623 *
624 * @param int|string $field_id Field ID.
625 * @param int|string $form_id Form ID.
626 *
627 * @return array|false
628 */
629 public static function duplicate_single_field( $field_id, $form_id ) {
630 $copy_field = self::getOne( $field_id );
631
632 if ( ! $copy_field ) {
633 return false;
634 }
635
636 do_action( 'frm_duplicate_field', $copy_field, $form_id );
637 do_action( 'frm_duplicate_field_' . $copy_field->type, $copy_field, $form_id );
638
639 $values = array(
640 'id' => $copy_field->id,
641 );
642 FrmFieldsHelper::fill_field( $values, $copy_field, $copy_field->form_id );
643 $values = apply_filters( 'frm_prepare_single_field_for_duplication', $values );
644 $field_id = self::create( $values );
645
646 /**
647 * Fires after duplicating a field.
648 *
649 * @since 5.0.04
650 *
651 * @param array $args {
652 * The arguments.
653 *
654 * @type int $field_id New field ID.
655 * @type array $values Values before inserting.
656 * @type object $copy_field Copy field data.
657 * @type int $form_id Form ID.
658 * }
659 */
660 do_action( 'frm_after_duplicate_field', compact( 'field_id', 'values', 'copy_field', 'form_id' ) );
661
662 return compact( 'field_id', 'values' );
663 }
664
665 /**
666 * @param int|string $old_form_id
667 * @param int|string $form_id
668 * @param bool $copy_keys
669 * @param false|int $blog_id
670 *
671 * @return void
672 */
673 public static function duplicate( $old_form_id, $form_id, $copy_keys = false, $blog_id = false ) {
674 global $frm_duplicate_ids;
675
676 $where = array(
677 array(
678 'or' => 1,
679 'fi.form_id' => $old_form_id,
680 'fr.parent_form_id' => $old_form_id,
681 ),
682 );
683 $fields = self::getAll( $where, 'field_order', '', $blog_id );
684
685 foreach ( (array) $fields as $field ) {
686 $new_key = $copy_keys ? $field->field_key : '';
687
688 // phpcs:ignore Universal.Operators.StrictComparisons
689 if ( $copy_keys && substr( $field->field_key, - 1 ) == 2 ) {
690 $new_key = rtrim( $new_key, 2 );
691 }
692
693 $values = array();
694 FrmFieldsHelper::fill_field( $values, $field, $form_id, $new_key );
695
696 // If this is a repeating section, create new form
697 if ( self::is_repeating_field( $field ) ) {
698 // Create the repeatable form
699 $new_repeat_form_id = apply_filters(
700 'frm_create_repeat_form',
701 0,
702 array(
703 'parent_form_id' => $form_id,
704 'field_name' => $field->name,
705 )
706 );
707
708 // Save old form_select
709 $old_repeat_form_id = $field->field_options['form_select'];
710
711 // Update form_select for repeating field
712 $values['field_options']['form_select'] = $new_repeat_form_id;
713 }
714
715 // If this is a field inside of a repeating section, associate it with the correct form
716 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
717 $values['form_id'] = $new_repeat_form_id;
718 }
719
720 $values['description'] = FrmFieldsHelper::switch_field_ids( $values['description'] );
721
722 $values = apply_filters( 'frm_duplicated_field', $values );
723 $new_id = self::create( $values );
724 $frm_duplicate_ids[ $field->id ] = $new_id;
725 $frm_duplicate_ids[ $field->field_key ] = $new_id;
726 unset( $field );
727 }//end foreach
728 }
729
730 /**
731 * @param int|string $id
732 * @param array $values
733 *
734 * @return false|int
735 */
736 public static function update( $id, $values ) {
737 global $wpdb;
738
739 $id = absint( $id );
740 $values = FrmAppHelper::maybe_filter_array( $values, array( 'name', 'description' ) );
741
742 if ( isset( $values['field_key'] ) ) {
743 $values['field_key'] = FrmAppHelper::get_unique_key( $values['field_key'], $wpdb->prefix . 'frm_fields', 'field_key', $id );
744 }
745
746 if ( isset( $values['required'] ) ) {
747 $values['required'] = (int) $values['required'];
748 }
749
750 self::preserve_format_option_backslashes( $values );
751
752 if ( isset( $values['type'] ) ) {
753 if ( 'dropdown' === $values['type'] ) {
754 // To avoid conflicts with security plugins the value "dropdown" is sent for select fields.
755 // This is because "select" gets matched for SQL injection attempts.
756 $values['type'] = 'select';
757 }
758
759 /**
760 * @since 6.9 The Field ID param was added.
761 *
762 * @param array $values
763 * @param int $id Field ID.
764 */
765 $values = apply_filters( 'frm_clean_' . $values['type'] . '_field_options_before_update', $values, $id );
766
767 if ( $values['type'] === 'hidden' && isset( $values['field_options'] ) && isset( $values['field_options']['clear_on_focus'] ) ) {
768 // Don't keep the old placeholder setting for hidden fields
769 $values['field_options']['clear_on_focus'] = 0;
770 }
771 }
772
773 // Serialize array values
774 foreach ( array( 'field_options', 'options' ) as $opt ) {
775 if ( ! isset( $values[ $opt ] ) || ! is_array( $values[ $opt ] ) ) {
776 continue;
777 }
778
779 if ( 'field_options' === $opt ) {
780 $values[ $opt ] = self::maybe_filter_options( $values[ $opt ] );
781 }
782
783 $values[ $opt ] = serialize( $values[ $opt ] );
784 }
785
786 if ( isset( $values['default_value'] ) && is_array( $values['default_value'] ) ) {
787 $values['default_value'] = json_encode( $values['default_value'] );
788 }
789
790 $query_results = $wpdb->update( $wpdb->prefix . 'frm_fields', $values, array( 'id' => $id ) );
791 $form_id = 0;
792
793 if ( isset( $values['form_id'] ) ) {
794 $form_id = absint( $values['form_id'] );
795 } else {
796 $field = self::getOne( $id );
797
798 if ( $field ) {
799 $form_id = $field->form_id;
800 }
801 unset( $field );
802 }
803 unset( $values );
804
805 if ( $query_results ) {
806 wp_cache_delete( $id, 'frm_field' );
807
808 if ( $form_id ) {
809 self::delete_form_transient( $form_id );
810 }
811 }
812
813 return $query_results;
814 }
815
816 /**
817 * Keep backslashes in the phone format option
818 *
819 * @since 2.0.8
820 *
821 * @param array $values Pass by reference.
822 *
823 * @return void
824 */
825 private static function preserve_format_option_backslashes( &$values ) {
826 if ( isset( $values['field_options']['format'] ) ) {
827 $values['field_options']['format'] = FrmAppHelper::preserve_backslashes( $values['field_options']['format'] );
828 }
829 }
830
831 /**
832 * @param int|string $id
833 *
834 * @return bool|int
835 */
836 public static function destroy( $id ) {
837 global $wpdb;
838
839 do_action( 'frm_before_destroy_field', $id );
840
841 wp_cache_delete( $id, 'frm_field' );
842 $field = self::getOne( $id );
843
844 if ( ! $field ) {
845 return false;
846 }
847
848 self::delete_form_transient( $field->form_id );
849
850 $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_item_metas WHERE field_id=%d', $id ) );
851
852 return $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_fields WHERE id=%d', $id ) );
853 }
854
855 /**
856 * @param int|string $form_id
857 *
858 * @return void
859 */
860 public static function delete_form_transient( $form_id ) {
861 $form_id = absint( $form_id );
862 delete_transient( 'frm_form_fields_' . $form_id . 'excludeinclude' );
863 delete_transient( 'frm_form_fields_' . $form_id . 'includeinclude' );
864 delete_transient( 'frm_form_fields_' . $form_id . 'includeexclude' );
865 delete_transient( 'frm_form_fields_' . $form_id . 'excludeexclude' );
866
867 global $wpdb;
868 $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
869
870 FrmDb::cache_delete_group( 'frm_field' );
871
872 $form = FrmForm::getOne( $form_id );
873
874 if ( $form && $form->parent_form_id && (int) $form->parent_form_id !== $form_id ) {
875 self::delete_form_transient( $form->parent_form_id );
876 }
877 }
878
879 /**
880 * If $field is numeric, get the field object
881 *
882 * @param int|object|string $field
883 *
884 * @return void
885 */
886 public static function maybe_get_field( &$field ) {
887 if ( ! is_object( $field ) ) {
888 $field = self::getOne( $field );
889 }
890 }
891
892 /**
893 * @param int|string $id The field id or key.
894 * @param bool $filter When true, run the frm_field filter.
895 *
896 * @return object|null
897 */
898 public static function getOne( $id, $filter = false ) {
899 if ( ! $id ) {
900 return null;
901 }
902
903 global $wpdb;
904
905 $where = is_numeric( $id ) ? 'id=%d' : 'field_key=%s';
906 $query = $wpdb->prepare( 'SELECT * FROM ' . $wpdb->prefix . 'frm_fields WHERE ' . $where, $id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
907
908 $results = FrmDb::check_cache( $id, 'frm_field', $query, 'get_row', 0 );
909
910 if ( ! $results ) {
911 self::filter_field( $filter, $results );
912 return $results;
913 }
914
915 if ( is_numeric( $id ) ) {
916 FrmDb::set_cache( $results->field_key, $results, 'frm_field' );
917 } elseif ( $results ) {
918 FrmDb::set_cache( $results->id, $results, 'frm_field' );
919 }
920
921 self::prepare_options( $results );
922 self::filter_field( $filter, $results );
923
924 return wp_unslash( $results );
925 }
926
927 /**
928 * @since 3.06.01
929 *
930 * @param bool $filter When true, run the frm_field filter.
931 * @param object $results
932 *
933 * @return void
934 */
935 private static function filter_field( $filter, &$results ) {
936 if ( $filter ) {
937 /**
938 * @since 3.06.01
939 */
940 $results = apply_filters( 'frm_field', $results );
941 }
942 }
943
944 /**
945 * Get the field type by key or id
946 *
947 * @param int|string $id The field id or key.
948 * @param mixed $col The name of the column in the fields database table.
949 *
950 * @return mixed
951 */
952 public static function get_type( $id, $col = 'type' ) {
953 $field = FrmDb::check_cache( $id, 'frm_field' );
954
955 if ( $field ) {
956 return $field->{$col};
957 }
958
959 $where = is_numeric( $id ) ? array( 'id' => $id ) : array( 'field_key' => $id );
960
961 return FrmDb::get_var( 'frm_fields', $where, $col );
962 }
963
964 /**
965 * @param int|string $form_id
966 * @param string $type
967 * @param int|string $limit
968 * @param string $inc_sub
969 *
970 * @return array|object
971 */
972 public static function get_all_types_in_form( $form_id, $type, $limit = '', $inc_sub = 'exclude' ) {
973 if ( ! $form_id ) {
974 return array();
975 }
976
977 $results = self::get_fields_from_transients(
978 $form_id,
979 array(
980 'inc_embed' => $inc_sub,
981 'inc_repeat' => $inc_sub,
982 )
983 );
984
985 if ( $results ) {
986 $fields = array();
987 $count = 0;
988
989 foreach ( $results as $result ) {
990 if ( $type !== $result->type ) {
991 continue;
992 }
993
994 $fields[ $result->id ] = $result;
995 ++$count;
996
997 // phpcs:ignore Universal.Operators.StrictComparisons
998 if ( $limit == 1 ) {
999 $fields = $result;
1000 break;
1001 }
1002
1003 if ( $limit && $count >= $limit ) {
1004 break;
1005 }
1006
1007 unset( $result );
1008 }
1009
1010 return wp_unslash( $fields );
1011 }//end if
1012
1013 self::$use_cache = false;
1014
1015 $where = array(
1016 'fi.form_id' => (int) $form_id,
1017 'fi.type' => $type,
1018 );
1019 self::maybe_include_repeating_fields( $inc_sub, $where );
1020 $results = self::getAll( $where, 'field_order', $limit );
1021 self::$use_cache = true;
1022 self::include_sub_fields( $results, $inc_sub, $type, $form_id );
1023
1024 return $results;
1025 }
1026
1027 /**
1028 * @param int|string $form_id
1029 * @param int|string $limit
1030 * @param string $inc_embed
1031 * @param string $inc_repeat
1032 *
1033 * @return array
1034 */
1035 public static function get_all_for_form( $form_id, $limit = '', $inc_embed = 'exclude', $inc_repeat = 'include' ) {
1036 if ( ! (int) $form_id ) {
1037 return array();
1038 }
1039
1040 $results = self::get_fields_from_transients( $form_id, compact( 'inc_embed', 'inc_repeat' ) );
1041
1042 if ( $results ) {
1043 if ( ! $limit ) {
1044 return $results;
1045 }
1046
1047 $fields = array();
1048 $count = 0;
1049
1050 foreach ( $results as $result ) {
1051 ++$count;
1052 $fields[ $result->id ] = $result;
1053
1054 if ( $limit && $count >= $limit ) {
1055 break;
1056 }
1057 }
1058
1059 return $fields;
1060 }
1061
1062 self::$use_cache = false;
1063
1064 $where = array( 'fi.form_id' => absint( $form_id ) );
1065 self::maybe_include_repeating_fields( $inc_repeat, $where );
1066 $results = self::getAll( $where, 'field_order', $limit );
1067
1068 self::$use_cache = true;
1069
1070 self::include_sub_fields( $results, $inc_embed, 'all', $form_id );
1071
1072 if ( ! $limit ) {
1073 self::set_field_transient( $results, $form_id, 0, compact( 'inc_embed', 'inc_repeat' ) );
1074 }
1075
1076 return $results;
1077 }
1078
1079 /**
1080 * If repeating fields should be included, adjust $where accordingly
1081 *
1082 * @param string $inc_repeat
1083 * @param array $where Pass by reference.
1084 *
1085 * @return void
1086 */
1087 private static function maybe_include_repeating_fields( $inc_repeat, &$where ) {
1088 if ( $inc_repeat !== 'include' ) {
1089 return;
1090 }
1091
1092 $form_id = $where['fi.form_id'];
1093 $where[] = array(
1094 'or' => 1,
1095 'fi.form_id' => $form_id,
1096 'fr.parent_form_id' => $form_id,
1097 );
1098 unset( $where['fi.form_id'] );
1099 }
1100
1101 /**
1102 * @param array $results
1103 * @param string $inc_embed
1104 * @param string $type
1105 * @param int|string $form_id
1106 *
1107 * @return void
1108 */
1109 public static function include_sub_fields( &$results, $inc_embed, $type = 'all', $form_id = '' ) {
1110 $no_sub_forms = ! $results && $type === 'all';
1111
1112 if ( 'include' !== $inc_embed || $no_sub_forms ) {
1113 return;
1114 }
1115
1116 $form_fields = $results;
1117 $should_get_subforms = $type !== 'all' && $type !== 'form' && $form_id;
1118
1119 if ( $should_get_subforms ) {
1120 $form_fields = self::get_all_types_in_form( $form_id, 'form' );
1121 }
1122
1123 $index_offset = 1;
1124
1125 foreach ( $form_fields as $k => $field ) {
1126 if ( 'form' !== $field->type || ! isset( $field->field_options['form_select'] ) ) {
1127 continue;
1128 }
1129
1130 if ( $type === 'all' ) {
1131 $sub_fields = self::get_all_for_form( $field->field_options['form_select'] );
1132 } else {
1133 $sub_fields = self::get_all_types_in_form( $field->field_options['form_select'], $type );
1134 }
1135
1136 if ( ! empty( $sub_fields ) ) {
1137 $index = $k + $index_offset;
1138 $index_offset += count( $sub_fields );
1139 array_splice( $results, $index, 0, $sub_fields );
1140 }
1141 unset( $field, $sub_fields );
1142 }
1143 }
1144
1145 /**
1146 * @param array|string $where
1147 * @param string $order_by
1148 * @param string $limit
1149 * @param false|int $blog_id
1150 *
1151 * @return array|object|null
1152 */
1153 public static function getAll( $where = array(), $order_by = '', $limit = '', $blog_id = false ) {
1154 $cache_key = FrmAppHelper::maybe_json_encode( $where ) . $order_by . 'l' . $limit . 'b' . $blog_id;
1155
1156 if ( self::$use_cache ) {
1157 // Make sure old cache doesn't get saved as a transient
1158 $results = wp_cache_get( $cache_key, 'frm_field' );
1159
1160 if ( false !== $results ) {
1161 return wp_unslash( $results );
1162 }
1163 }
1164
1165 global $wpdb;
1166
1167 if ( $blog_id && is_multisite() ) {
1168 $prefix = $wpdb->get_blog_prefix( $blog_id );
1169 $table_name = $prefix . 'frm_fields';
1170 $form_table_name = $prefix . 'frm_forms';
1171 } else {
1172 $table_name = $wpdb->prefix . 'frm_fields';
1173 $form_table_name = $wpdb->prefix . 'frm_forms';
1174 }
1175
1176 if ( $order_by && ! str_contains( $order_by, 'ORDER BY' ) ) {
1177 $order_by = ' ORDER BY ' . $order_by;
1178 }
1179
1180 $limit = FrmDb::esc_limit( $limit );
1181 $query = "SELECT fi.*, fr.name as form_name FROM {$table_name} fi JOIN {$form_table_name} fr ON fi.form_id=fr.id";
1182 $query_type = $limit === ' LIMIT 1' || $limit == 1 ? 'row' : 'results'; // phpcs:ignore Universal.Operators.StrictComparisons
1183
1184 if ( is_array( $where ) ) {
1185 $args = array(
1186 'order_by' => $order_by,
1187 'limit' => $limit,
1188 );
1189 $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 );
1190 } else {
1191 // If the query is not an array, then it has already been prepared
1192 $query .= FrmDb::prepend_and_or_where( ' WHERE ', $where ) . $order_by . $limit;
1193
1194 $function_name = $query_type === 'row' ? 'get_row' : 'get_results';
1195 $results = $wpdb->$function_name( $query );
1196 }
1197 unset( $where );
1198
1199 self::format_field_results( $results );
1200
1201 FrmDb::set_cache( $cache_key, $results, 'frm_field' );
1202
1203 return wp_unslash( $results );
1204 }
1205
1206 /**
1207 * @since 2.0.8
1208 *
1209 * @param array|object|null $results Results.
1210 *
1211 * @return void
1212 */
1213 private static function format_field_results( &$results ) {
1214 if ( is_array( $results ) ) {
1215 foreach ( $results as $r_key => $result ) {
1216 self::add_slashes_to_format_before_setting_field_cache( $result );
1217
1218 FrmDb::set_cache( $result->id, $result, 'frm_field' );
1219 FrmDb::set_cache( $result->field_key, $result, 'frm_field' );
1220
1221 self::prepare_options( $result );
1222 $results[ $r_key ]->field_options = $result->field_options;
1223 $results[ $r_key ]->options = $result->options;
1224 $results[ $r_key ]->default_value = $result->default_value;
1225
1226 unset( $r_key, $result );
1227 }
1228 } elseif ( $results ) {
1229 FrmDb::set_cache( $results->id, $results, 'frm_field' );
1230 FrmDb::set_cache( $results->field_key, $results, 'frm_field' );
1231
1232 self::prepare_options( $results );
1233 }
1234 }
1235
1236 /**
1237 * When $result->field_options is an array and not a serialized string there is only a single backslash.
1238 * Cached results are unslashed in FrmField::getAll, so we need to make sure that the cached object has an extra backslash.
1239 * Otherwise the backslash is stripped away on load.
1240 *
1241 * @since 6.15
1242 *
1243 * @param stdClass $result
1244 *
1245 * @return void
1246 */
1247 private static function add_slashes_to_format_before_setting_field_cache( $result ) {
1248 if ( ! isset( $result->field_options ) || ! is_array( $result->field_options ) || empty( $result->field_options['format'] ) ) {
1249 return;
1250 }
1251
1252 $result->field_options['format'] = addslashes( $result->field_options['format'] );
1253 }
1254
1255 /**
1256 * Unserialize all the serialized field data
1257 *
1258 * @since 2.0
1259 *
1260 * @param object $results
1261 *
1262 * @return void
1263 */
1264 private static function prepare_options( &$results ) {
1265 FrmAppHelper::unserialize_or_decode( $results->field_options );
1266 FrmAppHelper::unserialize_or_decode( $results->options );
1267
1268 // Allow a single box to be checked for the default value.
1269 $before = $results->default_value;
1270
1271 $field_object = FrmFieldFactory::get_field_type( $results->type );
1272
1273 if ( ! $field_object->should_unserialize_value() ) {
1274 return;
1275 }
1276
1277 FrmAppHelper::unserialize_or_decode( $results->default_value );
1278
1279 if ( $before === $results->default_value && is_string( $before ) && str_starts_with( $before, '["' ) ) {
1280 $results->default_value = FrmAppHelper::maybe_json_decode( $results->default_value );
1281 }
1282 }
1283
1284 /**
1285 * If a form has too many fields, they won't all save into a single transient.
1286 * We'll break them into groups of 200
1287 *
1288 * @since 2.0.1
1289 *
1290 * @param int|string $form_id Form ID.
1291 * @param array $args Additional arguments.
1292 *
1293 * @return array
1294 */
1295 private static function get_fields_from_transients( $form_id, $args ) {
1296 $fields = array();
1297 self::get_next_transient( $fields, 'frm_form_fields_' . $form_id . $args['inc_embed'] . $args['inc_repeat'] );
1298
1299 return $fields;
1300 }
1301
1302 /**
1303 * Called by get_fields_from_transients
1304 *
1305 * @since 2.0.1
1306 *
1307 * @param array $fields Array of fields.
1308 * @param string $base_name Base name.
1309 * @param int $next Next transient number.
1310 *
1311 * @return void
1312 */
1313 private static function get_next_transient( &$fields, $base_name, $next = 0 ) {
1314 $name = $next ? $base_name . $next : $base_name;
1315 $next_fields = get_transient( $name );
1316
1317 if ( ! $next_fields ) {
1318 return;
1319 }
1320
1321 $fields = array_merge( $fields, $next_fields );
1322
1323 if ( count( $next_fields ) < self::$transient_size ) {
1324 return;
1325 }
1326
1327 // If this transient is full, check for another
1328 ++$next;
1329 self::get_next_transient( $fields, $base_name, $next );
1330 }
1331
1332 /**
1333 * Save the transients in chunks for large forms
1334 *
1335 * @since 2.0.1
1336 *
1337 * @param array $fields Array of fields.
1338 * @param int|string $form_id Form ID.
1339 * @param int $next Next transient number.
1340 * @param array $args Additional arguments.
1341 *
1342 * @return void
1343 */
1344 private static function set_field_transient( &$fields, $form_id, $next = 0, $args = array() ) {
1345 $base_name = 'frm_form_fields_' . $form_id . $args['inc_embed'] . $args['inc_repeat'];
1346 $field_chunks = array_chunk( $fields, self::$transient_size );
1347
1348 foreach ( $field_chunks as $field ) {
1349 $name = $next ? $base_name . $next : $base_name;
1350 $set = set_transient( $name, $field, 60 * 60 * 6 );
1351
1352 if ( ! $set ) {
1353 // The transient didn't save
1354 if ( $name !== $base_name ) {
1355 // If the first saved an others fail, this will show an incomplete form
1356 self::delete_form_transient( $form_id );
1357 }
1358
1359 return;
1360 }
1361
1362 ++$next;
1363 }
1364 }
1365
1366 /**
1367 * @param string $type
1368 *
1369 * @return bool
1370 */
1371 public static function is_no_save_field( $type ) {
1372 return in_array( $type, self::no_save_fields(), true );
1373 }
1374
1375 /**
1376 * @return string[]
1377 */
1378 public static function no_save_fields() {
1379 return array( 'divider', 'end_divider', 'captcha', 'break', 'html', 'form', 'summary', FrmSubmitHelper::FIELD_TYPE );
1380 }
1381
1382 /**
1383 * Check if this field can hold an array of values
1384 *
1385 * @since 2.0.9
1386 *
1387 * @param array|object $field
1388 *
1389 * @return bool
1390 */
1391 public static function is_field_with_multiple_values( $field ) {
1392 if ( ! $field ) {
1393 return false;
1394 }
1395
1396 $field_type = self::get_original_field_type( $field );
1397
1398 return self::is_checkbox( $field ) || $field_type === 'address' || self::is_multiple_select( $field );
1399 }
1400
1401 /**
1402 * @since 3.0
1403 *
1404 * @param array|object $field
1405 *
1406 * @return string
1407 */
1408 public static function get_field_type( $field ) {
1409 return is_array( $field ) ? $field['type'] : $field->type;
1410 }
1411
1412 /**
1413 * @since 3.0
1414 *
1415 * @param array|object $field
1416 *
1417 * @return string
1418 */
1419 public static function get_original_field_type( $field ) {
1420 $field_type = self::get_field_type( $field );
1421 $original_type = self::get_option( $field, 'original_type' );
1422
1423 if ( $original_type && $original_type !== $field_type ) {
1424 // Check the original type for arrays.
1425 return $original_type;
1426 }
1427
1428 return $field_type;
1429 }
1430
1431 /**
1432 * Check if this is a multiselect dropdown field
1433 *
1434 * @since 2.0.9
1435 *
1436 * @param array|object $field Field object.
1437 *
1438 * @return bool
1439 */
1440 public static function is_multiple_select( $field ) {
1441 $is_multiple = self::is_option_true( $field, 'multiple' ) && self::is_field_type( $field, 'select' ) && self::get_field_type( $field ) !== 'hidden';
1442 return apply_filters( 'frm_is_multiple_select', $is_multiple, $field );
1443 }
1444
1445 /**
1446 * Check if a field is read only. Read only can be set in the field options,
1447 * but disabled with the shortcode options
1448 *
1449 * @since 2.0.9
1450 *
1451 * @param array|object $field
1452 *
1453 * @return bool
1454 */
1455 public static function is_read_only( $field ) {
1456 global $frm_vars;
1457 return self::is_option_true( $field, 'read_only' ) && ( ! isset( $frm_vars['readonly'] ) || $frm_vars['readonly'] !== 'disabled' );
1458 }
1459
1460 /**
1461 * @since 2.0.9
1462 *
1463 * @param array $field
1464 *
1465 * @return bool
1466 */
1467 public static function is_required( $field ) {
1468 $required = $field['required'] != '0'; // phpcs:ignore Universal.Operators.StrictComparisons
1469
1470 /**
1471 * @param bool $required
1472 * @param array $field
1473 */
1474 return (bool) apply_filters( 'frm_is_field_required', $required, $field );
1475 }
1476
1477 /**
1478 * @since 2.0.9
1479 *
1480 * @param array|object $field
1481 * @param string $option
1482 *
1483 * @return bool
1484 */
1485 public static function is_option_true( $field, $option ) {
1486 if ( is_array( $field ) ) {
1487 return self::is_option_true_in_array( $field, $option );
1488 }
1489 return self::is_option_true_in_object( $field, $option );
1490 }
1491
1492 /**
1493 * @since 2.0.9
1494 *
1495 * @param array|object $field
1496 * @param string $option
1497 *
1498 * @return bool
1499 */
1500 public static function is_option_empty( $field, $option ) {
1501 if ( is_array( $field ) ) {
1502 return self::is_option_empty_in_array( $field, $option );
1503 }
1504 return self::is_option_empty_in_object( $field, $option );
1505 }
1506
1507 /**
1508 * @param array $field
1509 * @param string $option
1510 *
1511 * @return bool
1512 */
1513 public static function is_option_true_in_array( $field, $option ) {
1514 return ! empty( $field[ $option ] );
1515 }
1516
1517 /**
1518 * @param object $field
1519 * @param string $option
1520 *
1521 * @return bool
1522 */
1523 public static function is_option_true_in_object( $field, $option ) {
1524 return ! empty( $field->field_options[ $option ] );
1525 }
1526
1527 /**
1528 * @param array $field
1529 * @param string $option
1530 *
1531 * @return bool
1532 */
1533 public static function is_option_empty_in_array( $field, $option ) {
1534 return empty( $field[ $option ] );
1535 }
1536
1537 /**
1538 * @param object $field
1539 * @param string $option
1540 *
1541 * @return bool
1542 */
1543 public static function is_option_empty_in_object( $field, $option ) {
1544 return empty( $field->field_options[ $option ] );
1545 }
1546
1547 /**
1548 * @param stdClass $field
1549 * @param string $option
1550 *
1551 * @return bool
1552 */
1553 public static function is_option_value_in_object( $field, $option ) {
1554 return isset( $field->field_options[ $option ] ) && $field->field_options[ $option ] != ''; // phpcs:ignore Universal.Operators.StrictComparisons
1555 }
1556
1557 /**
1558 * @since 2.0.18
1559 *
1560 * @param array|object $field
1561 * @param string $option
1562 *
1563 * @return mixed
1564 */
1565 public static function get_option( $field, $option ) {
1566 return is_array( $field ) ? self::get_option_in_array( $field, $option ) : self::get_option_in_object( $field, $option );
1567 }
1568
1569 /**
1570 * @param array $field
1571 * @param string $option
1572 *
1573 * @return mixed
1574 */
1575 public static function get_option_in_array( $field, $option ) {
1576 if ( isset( $field[ $option ] ) ) {
1577 $this_option = $field[ $option ];
1578 } elseif ( isset( $field['field_options'] ) && is_array( $field['field_options'] ) && isset( $field['field_options'][ $option ] ) ) {
1579 $this_option = $field['field_options'][ $option ];
1580 } else {
1581 $this_option = '';
1582 }
1583
1584 return $this_option;
1585 }
1586
1587 /**
1588 * @param object $field
1589 * @param string $option
1590 *
1591 * @return mixed
1592 */
1593 public static function get_option_in_object( $field, $option ) {
1594 return $field->field_options[ $option ] ?? '';
1595 }
1596
1597 /**
1598 * @since 2.0.09
1599 *
1600 * @param array|object $field
1601 *
1602 * @return bool
1603 */
1604 public static function is_repeating_field( $field ) {
1605 $is_repeating_field = is_array( $field ) ? 'divider' === $field['type'] : 'divider' === $field->type;
1606 return $is_repeating_field && self::is_option_true( $field, 'repeat' );
1607 }
1608
1609 /**
1610 * @param string $key
1611 *
1612 * @return int field id
1613 */
1614 public static function get_id_by_key( $key ) {
1615 $id = FrmDb::get_var( 'frm_fields', array( 'field_key' => sanitize_title( $key ) ) );
1616 return (int) $id;
1617 }
1618
1619 /**
1620 * @param string $id
1621 *
1622 * @return string|null
1623 */
1624 public static function get_key_by_id( $id ) {
1625 return FrmDb::get_var( 'frm_fields', array( 'id' => $id ), 'field_key' );
1626 }
1627
1628 /**
1629 * @param array|object $field
1630 *
1631 * @return bool
1632 */
1633 public static function is_image( $field ) {
1634 $type = self::get_field_type( $field );
1635 return $type === 'url' && self::get_option( $field, 'show_image' );
1636 }
1637
1638 /**
1639 * Check if field is radio or Dynamic radio
1640 *
1641 * @since 3.0
1642 *
1643 * @param array|object $field
1644 *
1645 * @return bool true if field type is radio or Dynamic radio
1646 */
1647 public static function is_radio( $field ) {
1648 return self::is_field_type( $field, 'radio' );
1649 }
1650
1651 /**
1652 * Check if field is checkbox or Dynamic checkbox
1653 *
1654 * @since 3.0
1655 *
1656 * @param array|object $field
1657 *
1658 * @return bool true if field type is checkbox or Dynamic checkbox
1659 */
1660 public static function is_checkbox( $field ) {
1661 return self::is_field_type( $field, 'checkbox' );
1662 }
1663
1664 /**
1665 * Check if field is checkbox or radio
1666 *
1667 * @since 3.0
1668 *
1669 * @param array|object $field
1670 * @param string $is_type Options include radio, checkbox, text.
1671 *
1672 * @return bool true if field type is checkbox or Dynamic checkbox
1673 */
1674 public static function is_field_type( $field, $is_type ) {
1675 $field_type = self::get_original_field_type( $field );
1676 $data_type = self::get_option( $field, 'data_type' );
1677 $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
1678
1679 /**
1680 * When a field type is checked, allow individual fields
1681 * to set the type.
1682 *
1683 * @since 4.04
1684 */
1685 return apply_filters( 'frm_is_field_type', $is_field_type, compact( 'field', 'is_type' ) );
1686 }
1687
1688 /**
1689 * Checks if the given field array is a combo field.
1690 *
1691 * @since 4.10.02
1692 *
1693 * @param array $field Field array.
1694 *
1695 * @return bool
1696 */
1697 public static function is_combo_field( $field ) {
1698 $field_type_obj = FrmFieldFactory::get_field_factory( $field );
1699 return ! empty( $field_type_obj->is_combo_field );
1700 }
1701 }
1702