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

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