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 / controllers / FrmFieldsController.php

FrmFieldsController.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More trunk, at classes/controllers/FrmFieldsController.php

1,282 lines 36.1 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 FrmFieldsController {
7
8 /**
9 * This is stored statically so we can re-use this data for every field.
10 *
11 * @var FrmFieldSelectionData|null
12 */
13 private static $field_selection_data;
14
15 /**
16 * Render the fields the form builder asked for over ajax.
17 *
18 * The browser sends field ids only. The fields themselves are read from the form, which is one
19 * indexed query shared with the rest of the request through the field cache, and cheaper than
20 * shipping every field's data down to the page and straight back up again.
21 *
22 * @return void
23 */
24 public static function load_field() {
25 FrmAppHelper::permission_check( 'frm_edit_forms' );
26 check_ajax_referer( 'frm_ajax', 'nonce' );
27
28 $field_ids = FrmAppHelper::get_post_param( 'field_ids', array(), 'absint' );
29 $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' );
30
31 if ( ! $form_id || ! is_array( $field_ids ) || ! $field_ids ) {
32 wp_die();
33 }
34
35 $_GET['page'] = 'formidable';
36 $fields = self::get_builder_fields_by_id( $form_id );
37 $values = array(
38 'id' => $form_id,
39 'doing_ajax' => true,
40 );
41 $field_html = array();
42
43 foreach ( $field_ids as $field_id ) {
44 if ( ! isset( $fields[ $field_id ] ) ) {
45 // This field may have already been loaded, or is no longer in the form.
46 continue;
47 }
48
49 $field = $fields[ $field_id ];
50
51 ob_start();
52 self::load_single_field( $field, $values );
53
54 $field_html[ $field_id ] = array(
55 // The type travels with the html so the js can report it to frm_ajax_loaded_field
56 // listeners without a copy of the field.
57 'type' => $field->type,
58 'html' => ob_get_clean(),
59 );
60 }//end foreach
61
62 echo json_encode( $field_html );
63
64 wp_die();
65 }
66
67 /**
68 * Get a form's fields, as the form builder sees them, indexed by field id.
69 *
70 * @since 6.35
71 *
72 * @param int $form_id
73 *
74 * @return array Field objects keyed by field id. Empty if the form is gone.
75 */
76 private static function get_builder_fields_by_id( $form_id ) {
77 $form = FrmForm::getOne( $form_id );
78
79 if ( ! $form ) {
80 return array();
81 }
82
83 $fields = FrmField::get_all_for_form( $form_id );
84
85 /** This filter is documented in classes/controllers/FrmFormsController.php */
86 $fields = apply_filters( 'frm_fields_in_form_builder', $fields, compact( 'form' ) );
87
88 $fields_by_id = array();
89
90 foreach ( (array) $fields as $field ) {
91 if ( is_object( $field ) && ! empty( $field->id ) ) {
92 $fields_by_id[ (int) $field->id ] = $field;
93 }
94 }
95
96 return $fields_by_id;
97 }
98
99 /**
100 * Create a new field with ajax
101 */
102 public static function create() {
103 FrmAppHelper::permission_check( 'frm_edit_forms' );
104 check_ajax_referer( 'frm_ajax', 'nonce' );
105
106 $field_type = FrmAppHelper::get_post_param( 'field_type', '', 'sanitize_text_field' );
107 $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' );
108 $field_options = FrmAppHelper::get_post_param( 'field_options', array(), 'wp_kses_post' );
109
110 do_action( 'frm_before_create_field', $field_type, $form_id );
111
112 $field = self::include_new_field( $field_type, $form_id, $field_options );
113
114 // This hook will allow for multiple fields to be added at once
115 do_action( 'frm_after_field_created', $field, $form_id );
116
117 wp_die();
118 }
119
120 /**
121 * Set up and create a new field
122 *
123 * @param string $field_type
124 * @param int $form_id
125 * @param array $field_options
126 *
127 * @return array|false
128 */
129 public static function include_new_field( $field_type, $form_id, $field_options = array() ) {
130 $field_values = FrmFieldsHelper::setup_new_vars( $field_type, $form_id );
131
132 if ( $field_options ) {
133 $field_values['field_options'] = array_merge( $field_values['field_options'], $field_options );
134 }
135
136 // When a new field is added to the form, flag it as draft and hide it from the front-end.
137 $field_values['field_options']['draft'] = 1;
138
139 /**
140 * @param array $field_values
141 */
142 $field_values = apply_filters( 'frm_before_field_created', $field_values );
143 $field_id = FrmField::create( $field_values );
144
145 if ( ! $field_id ) {
146 return false;
147 }
148
149 $field = self::get_field_array_from_id( $field_id );
150 $values = array();
151
152 if ( FrmAppHelper::pro_is_installed() ) {
153 $values['post_type'] = FrmProFormsHelper::post_type( $form_id );
154 $parent_form_id = FrmDb::get_var( 'frm_forms', array( 'id' => $form_id ), 'parent_form_id' );
155
156 if ( $parent_form_id ) {
157 $field['parent_form_id'] = $parent_form_id;
158 }
159 }
160
161 self::load_single_field( $field, $values, $form_id );
162
163 return $field;
164 }
165
166 public static function duplicate() {
167 FrmAppHelper::permission_check( 'frm_edit_forms' );
168 check_ajax_referer( 'frm_ajax', 'nonce' );
169
170 $field_id = FrmAppHelper::get_post_param( 'field_id', 0, 'absint' );
171 $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' );
172 $new_field = FrmField::duplicate_single_field( $field_id, $form_id );
173
174 if ( is_array( $new_field ) && ! empty( $new_field['field_id'] ) ) {
175 self::load_single_field( $new_field['field_id'], $new_field['values'], $form_id );
176 }
177
178 wp_die();
179 }
180
181 /**
182 * @since 3.0
183 *
184 * @param int $field_id
185 *
186 * @return array
187 */
188 public static function get_field_array_from_id( $field_id ) {
189 $field = FrmField::getOne( $field_id );
190 return FrmFieldsHelper::setup_edit_vars( $field );
191 }
192
193 /**
194 * @since 3.0
195 *
196 * @param array|int|object|string $field_object
197 * @param array $values
198 * @param int $form_id
199 *
200 * @return void
201 */
202 public static function load_single_field( $field_object, $values, $form_id = 0 ) {
203 global $frm_vars;
204 $frm_vars['is_admin'] = true;
205
206 if ( is_numeric( $field_object ) ) {
207 $field_object = FrmField::getOne( $field_object );
208 } elseif ( is_array( $field_object ) ) {
209 $field = $field_object;
210 $field_object = FrmField::getOne( $field['id'] );
211 }
212
213 $field_obj = FrmFieldFactory::get_field_factory( $field_object );
214 $display = self::display_field_options( array(), $field_obj );
215 $ajax_loading = ! empty( $values['ajax_load'] );
216 $ajax_this_field = isset( $values['count'] ) && $values['count'] > 10 && ! in_array( $field_object->type, array( 'divider', 'end_divider' ), true );
217
218 if ( $ajax_loading && $ajax_this_field ) {
219 $li_classes = self::get_classes_for_builder_field( array(), $display, $field_obj );
220 include FrmAppHelper::plugin_path() . '/classes/views/frm-fields/back-end/ajax-field-placeholder.php';
221 return;
222 }
223
224 if ( ! isset( $field ) && is_object( $field_object ) ) {
225 // Prefer the explicit form id. $values['id'] is not always a form id (for example when
226 // duplicating a field it is the copied field's id), so trusting it would set parent_form_id
227 // to a field id and break settings that resolve fields against the parent form.
228 $field_object->parent_form_id = $form_id ? $form_id : ( $values['id'] ?? $field_object->form_id );
229 $field = FrmFieldsHelper::setup_edit_vars( $field_object );
230 }
231
232 /**
233 * Filter for adding extra attributes to the field container.
234 *
235 * @since 6.23
236 *
237 * @param array $extra_field_attributes
238 * @param array $field
239 * @param array $display
240 */
241 $extra_field_attributes = apply_filters( 'frm_field_container_extra_attributes', array(), $field, $display );
242
243 $li_classes = self::get_classes_for_builder_field( $field, $display, $field_obj );
244 $li_classes .= ' ui-state-default widgets-holder-wrap';
245
246 require FrmAppHelper::plugin_path() . '/classes/views/frm-forms/add_field.php';
247 }
248
249 /**
250 * @since 3.0
251 *
252 * @param array $field
253 * @param array $display
254 * @param FrmFieldType $field_info
255 *
256 * @return string
257 */
258 private static function get_classes_for_builder_field( $field, $display, $field_info ) {
259 $li_classes = $field_info->form_builder_classes( $display['type'] );
260 $li_classes .= ' frm_form_field frmstart ';
261
262 $style_align_class = self::get_builder_field_style_align_class( $field, $field_info );
263
264 if ( $style_align_class ) {
265 $li_classes .= $style_align_class . ' ';
266 }
267
268 if ( isset( $field['classes'] ) ) {
269 $li_classes .= trim( $field['classes'] ) . ' ';
270 }
271
272 $li_classes .= 'frmend';
273
274 if ( $field ) {
275 return apply_filters( 'frm_build_field_class', $li_classes, $field );
276 }
277
278 return $li_classes;
279 }
280
281 /**
282 * Apply the active style alignment to a radio or checkbox builder preview on load.
283 *
284 * The per-field alignment setting only exists in Pro, so on load the style setting is
285 * used. When the field has an explicit alignment (Pro), the frm_build_field_class filter
286 * applies it instead. When Pro is not installed, any saved alignment is treated as empty.
287 *
288 * @since 6.32
289 *
290 * @param array $field
291 * @param FrmFieldType $field_info
292 *
293 * @return string
294 */
295 private static function get_builder_field_style_align_class( $field, $field_info ) {
296 if ( ! $field || ! is_array( $field ) || ( ! FrmField::is_radio( $field ) && ! FrmField::is_checkbox( $field ) ) ) {
297 return '';
298 }
299
300 $align = FrmAppHelper::pro_is_installed() ? FrmField::get_option( $field, 'align' ) : '';
301
302 if ( $align ) {
303 return '';
304 }
305
306 $align = FrmStylesHelper::get_align_from_active_style( $field );
307 $field_info->prepare_align_class( $align );
308
309 return $align;
310 }
311
312 public static function destroy() {
313 FrmAppHelper::permission_check( 'frm_edit_forms' );
314 check_ajax_referer( 'frm_ajax', 'nonce' );
315
316 $field_id = FrmAppHelper::get_post_param( 'field_id', 0, 'absint' );
317 FrmField::destroy( $field_id );
318 wp_die();
319 }
320
321 /**
322 * Field Options.
323 */
324 public static function import_options() {
325 FrmAppHelper::permission_check( 'frm_edit_forms' );
326 check_ajax_referer( 'frm_ajax', 'nonce' );
327
328 if ( ! is_admin() || ! current_user_can( 'frm_edit_forms' ) ) {
329 return;
330 }
331
332 $field_id = FrmAppHelper::get_param( 'field_id', '', 'post', 'absint' );
333 $field = FrmField::getOne( $field_id );
334 $bulk_edit_types = array( 'radio', 'checkbox', 'select' );
335
336 /**
337 * Filter to add new fields that will support import_options/Bulk Edit Options.
338 *
339 * @since 6.8.4
340 *
341 * @param array $bulk_edit_types
342 *
343 * @return array
344 */
345 $bulk_edit_types = apply_filters( 'frm_bulk_edit_field_types', $bulk_edit_types );
346
347 if ( ! in_array( $field->type, $bulk_edit_types, true ) ) {
348 return;
349 }
350
351 $field = FrmFieldsHelper::setup_edit_vars( $field );
352
353 $opts = FrmAppHelper::get_param( 'opts', '', 'post', 'wp_kses_post' );
354 $opts = explode( "\n", rtrim( $opts, "\n" ) );
355 $opts = array_map( 'trim', $opts );
356
357 $separate = FrmAppHelper::get_param( 'separate', '', 'post', 'sanitize_text_field' );
358 $field['separate_value'] = $separate === 'true';
359
360 if ( $field['separate_value'] ) {
361 foreach ( $opts as $opt_key => $opt ) {
362 if ( str_contains( $opt, '|' ) ) {
363 $vals = explode( '|', $opt );
364 $opts[ $opt_key ] = array(
365 'label' => trim( $vals[0] ),
366 'value' => trim( $vals[1] ),
367 );
368 unset( $vals );
369 }
370 unset( $opt_key, $opt );
371 }
372 }
373
374 // Keep other options after bulk update.
375 if ( ! empty( $field['field_options']['other'] ) ) {
376 $other_array = array();
377
378 foreach ( $field['options'] as $opt_key => $opt ) {
379 if ( FrmFieldsHelper::is_other_opt( $opt_key ) ) {
380 $other_array[ $opt_key ] = $opt;
381 }
382 unset( $opt_key, $opt );
383 }
384
385 if ( $other_array ) {
386 $opts = array_merge( $opts, $other_array );
387 }
388 }
389
390 $field['options'] = $opts;
391
392 FrmFieldsHelper::show_single_option( $field );
393
394 wp_die();
395 }
396
397 /**
398 * @since 4.0
399 *
400 * @param array $atts - Includes field array, field_obj, display array, values array.
401 *
402 * @return void
403 */
404 public static function load_single_field_settings( $atts ) {
405 $field = $atts['field'];
406 $field_obj = $atts['field_obj'];
407 $values = $atts['values'];
408 $display = $atts['display'];
409 unset( $atts );
410
411 if ( ! isset( $field['unique'] ) ) {
412 $field['unique'] = false;
413 }
414
415 if ( ! isset( $field['read_only'] ) ) {
416 $field['read_only'] = false;
417 }
418
419 $field_selection_data = self::maybe_define_field_selection_data();
420 $all_field_types = $field_selection_data->all_field_types;
421 $disabled_fields = $field_selection_data->disabled_fields;
422 $frm_settings = FrmAppHelper::get_settings();
423 $field_types = FrmFieldTypeOptionData::get_field_types( $field['type'] );
424
425 if ( ! isset( $all_field_types[ $field['type'] ] ) ) {
426 // Add fallback for an add-on field type that has been deactivated.
427 $all_field_types[ $field['type'] ] = array(
428 'name' => ucfirst( $field['type'] ),
429 'icon' => 'frmfont frm_pencil_icon',
430 );
431 } elseif ( ! is_array( $all_field_types[ $field['type'] ] ) ) {
432 // Fallback for fields added in a more basic way.
433 FrmFormsHelper::prepare_field_type( $all_field_types[ $field['type'] ] );
434 }
435
436 $type_name = $all_field_types[ $field['type'] ]['name'];
437
438 if ( $field['type'] === 'divider' && FrmField::is_option_true( $field, 'repeat' ) ) {
439 $type_name = $all_field_types['divider|repeat']['name'];
440 }
441
442 if ( $display['default'] ) {
443 $default_value_types = self::default_value_types( $field, compact( 'display' ) );
444 }
445
446 if ( $display['clear_on_focus'] && is_array( $field['placeholder'] ) ) {
447 $field['placeholder'] = implode( ', ', $field['placeholder'] );
448 }
449
450 $pro_is_installed = FrmAppHelper::pro_is_installed();
451
452 $unique_values_label_atts = array(
453 'for' => 'frm_uniq_field_' . $field['id'],
454 'class' => 'frm_help frm-mb-0',
455 'title' => __(
456 'Unique: Do not allow the same response multiple times. For example, if one user enters \'Joe\', then no one else will be allowed to enter the same name.',
457 'formidable'
458 ),
459 'data-trigger' => 'hover',
460 );
461
462 $read_only_label_atts = array(
463 'for' => 'frm_read_only_field_' . $field['id'],
464 'class' => 'frm_help frm-mb-0',
465 'title' => __( 'Read Only: Show this field but do not allow the field value to be edited from the front-end.', 'formidable' ),
466 'data-trigger' => 'hover',
467 );
468
469 if ( ! $pro_is_installed ) {
470 $visibility_upsell_atts = FrmSettingsUpsellHelper::add_upgrade_modal_atts(
471 array(
472 'id' => 'field_options_admin_only_' . $field['id'],
473 'readonly' => '1',
474 ),
475 'field_visibility',
476 __( 'Visibility options', 'formidable' ),
477 '/field-options/#kb-visibility'
478 );
479
480 $autocomplete_upsell_atts = FrmSettingsUpsellHelper::add_upgrade_modal_atts(
481 array( 'id' => 'field_options_autocomplete_' . $field['id'] ),
482 'autocomplete',
483 __( 'Autocomplete options', 'formidable' ),
484 '/email-address/#kb-autocomplete-attribute'
485 );
486
487 $before_after_content_upsell_atts = FrmSettingsUpsellHelper::add_upgrade_modal_atts(
488 array(
489 'type' => 'text',
490 'readonly' => '1',
491 ),
492 'before_after_contents',
493 __( 'Before and after field contents', 'formidable' ),
494 '/field-options/#kb-before-after-input'
495 );
496
497 $show_upsell_for_unique_value = in_array(
498 $field['type'],
499 array( 'checkbox', 'email', 'name', 'number', 'phone', 'radio', 'text', 'textarea', 'url' ),
500 true
501 );
502 $show_upsell_for_read_only = in_array( $field['type'], array( 'address', 'email', 'hidden', 'number', 'phone', 'radio', 'text', 'textarea', 'url' ), true );
503 $show_upsell_for_before_after_contents = in_array( $field['type'], array( 'email', 'number', 'phone', 'quantity', 'select', 'tag', 'text', 'total', 'url' ), true );
504 $show_upsell_for_autocomplete = in_array( $field['type'], array( 'text', 'email', 'number' ), true );
505 $show_upsell_for_visibility = $field['type'] !== 'hidden';
506
507 $unique_values_label_atts = FrmSettingsUpsellHelper::add_upgrade_modal_atts(
508 $unique_values_label_atts,
509 'unique_values',
510 __( 'Unique Values', 'formidable' ),
511 '/field-options/#kb-unique'
512 );
513 $read_only_label_atts = FrmSettingsUpsellHelper::add_upgrade_modal_atts(
514 $read_only_label_atts,
515 'read_only',
516 __( 'Read Only Values', 'formidable' ),
517 '/field-options/#kb-read-only'
518 );
519 }//end if
520
521 include FrmAppHelper::plugin_path() . '/classes/views/frm-fields/back-end/settings.php';
522 }
523
524 /**
525 * @since 6.9.1
526 *
527 * @return FrmFieldSelectionData
528 */
529 private static function maybe_define_field_selection_data() {
530 if ( ! isset( self::$field_selection_data ) ) {
531 self::$field_selection_data = new FrmFieldSelectionData();
532 }
533 return self::$field_selection_data;
534 }
535
536 /**
537 * Get the list of default value types that can be toggled in the builder.
538 *
539 * @since 4.0
540 *
541 * @param array $field
542 * @param array $atts
543 *
544 * @return array
545 */
546 private static function default_value_types( $field, $atts ) {
547 $types = array(
548 'default_value' => array(
549 'class' => '',
550 'icon' => 'frmfont frm_text2_icon',
551 'title' => __( 'Default Value', 'formidable' ),
552 'data' => array(
553 'frmshow' => '#default-value-for-',
554 ),
555 ),
556 'calc' => array(
557 'class' => 'frm_show_upgrade frm_noallow',
558 'title' => __( 'Calculate Value', 'formidable' ),
559 'icon' => 'frmfont frm_calculator_icon',
560 'data' => array(
561 'medium' => 'calculations',
562 'upgrade' => __( 'Calculator forms', 'formidable' ),
563 ),
564 'tooltip' => __( 'Automatically calculate the value of this field based on values from other fields.', 'formidable' ),
565 ),
566 'get_values_field' => array(
567 'class' => 'frm_show_upgrade frm_noallow',
568 'title' => __( 'Lookup', 'formidable' ),
569 'icon' => 'frmfont frm_search_icon',
570 'data' => array(
571 'medium' => 'lookup',
572 'upgrade' => __( 'Lookup fields', 'formidable' ),
573 ),
574 'tooltip' => __( 'Dynamically retrieve the value of this field from a lookup field.', 'formidable' ),
575 ),
576 );
577
578 $types = apply_filters( 'frm_default_value_types', $types, $atts );
579
580 // Set active class.
581 $settings = array_keys( $types );
582 $active = 'default_value';
583
584 if ( FrmAppHelper::pro_is_connected() ) {
585 foreach ( $settings as $type ) {
586 if ( ! empty( $field[ $type ] ) ) {
587 $active = $type;
588 }
589 }
590 }
591
592 $types[ $active ]['class'] .= ' current';
593 $types[ $active ]['current'] = true;
594
595 return $types;
596 }
597
598 /**
599 * @param string $type
600 *
601 * @return string
602 */
603 public static function change_type( $type ) {
604 $type_switch = array(
605 'scale' => 'radio',
606 'star' => 'radio',
607 '10radio' => 'radio',
608 'rte' => 'textarea',
609 'website' => 'url',
610 'image' => 'url',
611 );
612
613 if ( isset( $type_switch[ $type ] ) ) {
614 $type = $type_switch[ $type ];
615 }
616
617 $pro_fields = FrmField::pro_field_selection();
618 // We want to keep credit_card types as credit card types for Stripe Lite.
619 // The credit_card key is set for backward compatibility.
620 FrmField::remove_moved_field_types_from_pro( $pro_fields );
621
622 return array_key_exists( $type, $pro_fields ) ? 'text' : $type;
623 }
624
625 /**
626 * @param array $settings
627 * @param FrmFieldType|null $field_info
628 *
629 * @return array
630 */
631 public static function display_field_options( $settings, $field_info = null ) {
632 if ( $field_info ) {
633 $settings = $field_info->display_field_settings();
634
635 // Field appears to be protected but there is a __get function in FrmFieldType that makes this public.
636 $settings['field_data'] = $field_info->field;
637 }
638
639 return apply_filters( 'frm_display_field_options', $settings );
640 }
641
642 /**
643 * Display the format option
644 *
645 * @since 3.0
646 *
647 * @param array $field Field data.
648 * @param bool $is_hidden Whether the format option should be hidden.
649 *
650 * @return void
651 */
652 public static function show_format_option( $field, $is_hidden = false ) {
653 $attributes = array(
654 'class' => 'frm-has-modal',
655 'id' => 'frm-field-format-custom-' . $field['id'],
656 );
657
658 if ( $is_hidden ) {
659 $attributes['class'] .= ' frm_hidden';
660 }
661
662 include FrmAppHelper::plugin_path() . '/classes/views/frm-fields/back-end/value-format.php';
663 }
664
665 /**
666 * @param array $field
667 * @param bool $echo
668 *
669 * @return string
670 */
671 public static function input_html( $field, $echo = true ) {
672 $class = array();
673 self::add_input_classes( $field, $class );
674
675 $add_html = array();
676 self::add_html_size( $field, $add_html );
677 self::add_html_length( $field, $add_html );
678 self::add_html_placeholder( $field, $add_html, $class );
679 self::add_validation_messages( $field, $add_html );
680
681 $class = apply_filters( 'frm_field_classes', implode( ' ', $class ), $field );
682
683 FrmFormsHelper::add_html_attr( $class, 'class', $add_html );
684
685 self::add_shortcodes_to_html( $field, $add_html );
686 self::add_pattern_attribute( $field, $add_html );
687 self::add_currency_field_attributes( $field, $add_html );
688
689 $add_html = apply_filters( 'frm_field_extra_html', $add_html, $field );
690 $add_html = ' ' . implode( ' ', $add_html ) . ' ';
691
692 if ( $echo ) {
693 echo $add_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
694 }
695
696 return $add_html;
697 }
698
699 /**
700 * @param array $field
701 * @param array $class
702 *
703 * @return void
704 */
705 private static function add_input_classes( $field, array &$class ) {
706 if ( ! empty( $field['input_class'] ) ) {
707 $class[] = $field['input_class'];
708 }
709
710 if ( $field['type'] === 'hidden' || $field['type'] === 'user_id' ) {
711 return;
712 }
713
714 if ( isset( $field['size'] ) && $field['size'] > 0 ) {
715 $class[] = 'auto_width';
716 }
717 }
718
719 /**
720 * @param array $field
721 * @param array $add_html
722 *
723 * @return void
724 */
725 private static function add_html_size( $field, array &$add_html ) {
726 $size_fields = array(
727 'select',
728 'data',
729 'time',
730 'hidden',
731 'file',
732 'lookup',
733 );
734
735 if ( ! isset( $field['size'] ) || $field['size'] <= 0 || in_array( $field['type'], $size_fields, true ) ) {
736 return;
737 }
738
739 if ( FrmAppHelper::is_admin_page( 'formidable' ) ) {
740 return;
741 }
742
743 if ( is_numeric( $field['size'] ) ) {
744 $field['size'] .= 'px';
745 }
746
747 $important = apply_filters( 'frm_use_important_width', 1, $field );
748 // Note: This inline styling must stay since we cannot realistically set a class for every possible field size.
749 $add_html['style'] = 'style="width:' . esc_attr( $field['size'] ) . ( $important ? ' !important' : '' ) . '"';
750
751 self::add_html_cols( $field, $add_html );
752 }
753
754 /**
755 * @param array $field
756 * @param array $add_html
757 *
758 * @return void
759 */
760 private static function add_html_cols( $field, array &$add_html ) {
761 if ( ! in_array( $field['type'], array( 'textarea', 'rte' ), true ) ) {
762 return;
763 }
764
765 // Convert to cols for textareas.
766 $calc = array(
767 '' => 9,
768 'px' => 9,
769 'rem' => 0.444,
770 'em' => 0.544,
771 );
772
773 // Include "col" for valid html
774 $unit = trim( preg_replace( '/[0-9]+/', '', $field['size'] ) );
775
776 if ( ! isset( $calc[ $unit ] ) ) {
777 return;
778 }
779
780 $size = (float) str_replace( $unit, '', $field['size'] ) / $calc[ $unit ];
781 $add_html['cols'] = 'cols="' . absint( $size ) . '"';
782 }
783
784 /**
785 * @param array $field
786 * @param array $add_html
787 *
788 * @return void
789 */
790 private static function add_html_length( $field, array &$add_html ) {
791 // Check for max setting and if this field accepts maxlength.
792 $fields = array(
793 'textarea',
794 'rte',
795 'hidden',
796 'file',
797 );
798
799 if ( FrmField::is_option_empty( $field, 'max' ) || in_array( $field['type'], $fields, true ) ) {
800 return;
801 }
802
803 if ( FrmAppHelper::is_admin_page( 'formidable' ) ) {
804 // Don't load on form builder page.
805 return;
806 }
807
808 $add_html['maxlength'] = 'maxlength="' . esc_attr( $field['max'] ) . '"';
809 }
810
811 /**
812 * @param array $field
813 * @param array $add_html
814 * @param array $class
815 *
816 * @return void
817 */
818 private static function add_html_placeholder( $field, array &$add_html, array &$class ) {
819 // phpcs:ignore Universal.Operators.StrictComparisons
820 if ( $field['default_value'] != '' ) {
821 if ( is_array( $field['default_value'] ) ) {
822 $add_html['data-frmval'] = 'data-frmval="' . esc_attr( json_encode( $field['default_value'] ) ) . '"';
823 } else {
824 self::add_frmval_to_input( $field, $add_html );
825 }
826 }
827
828 $field['placeholder'] = self::prepare_placeholder( $field );
829
830 // phpcs:ignore Universal.Operators.StrictComparisons
831 if ( $field['placeholder'] == '' || is_array( $field['placeholder'] ) ) {
832 // Don't include a json placeholder
833 return;
834 }
835
836 self::add_placeholder_to_input( $field, $add_html );
837 }
838
839 /**
840 * Prepares field placeholder.
841 *
842 * @since 5.4 This doesn't call `FrmFieldsController::get_default_value_from_name()` anymore.
843 *
844 * @param array $field Field array.
845 *
846 * @return string
847 */
848 private static function prepare_placeholder( $field ) {
849 return $field['placeholder'] ?? '';
850 }
851
852 /**
853 * If the label position is "inside",
854 * get the label to use as the placeholder
855 *
856 * @since 2.05
857 * @since 5.4 Remove the logic code for "inside" label position.
858 *
859 * @param array $field
860 *
861 * @return string
862 */
863 public static function get_default_value_from_name( $field ) {
864 return '';
865 }
866
867 /**
868 * Maybe add a blank placeholder option before any options
869 * in a dropdown.
870 *
871 * @since 4.04
872 *
873 * @param array|object $field
874 *
875 * @return bool True if placeholder was added.
876 */
877 public static function add_placeholder_to_select( $field ) {
878 $placeholder = FrmField::get_option( $field, 'placeholder' );
879
880 if ( ! $placeholder ) {
881 $placeholder = self::get_default_value_from_name( $field );
882 }
883
884 $use_placeholder = $placeholder;
885 $autocomplete = FrmField::get_option( $field, 'autocom' );
886
887 if ( $autocomplete ) {
888 $use_chosen = ! is_callable( 'FrmProAppHelper::use_chosen_js' ) || FrmProAppHelper::use_chosen_js();
889
890 if ( $use_chosen ) {
891 $use_placeholder = '';
892 }
893 }
894
895 if ( $placeholder !== '' ) {
896 $placeholder_attributes = array(
897 'class' => 'frm-select-placeholder',
898 'value' => '',
899 'data-placeholder' => 'true',
900 );
901
902 FrmHtmlHelper::echo_dropdown_option( $use_placeholder, false, $placeholder_attributes );
903 return true;
904 }
905
906 return false;
907 }
908
909 /**
910 * Use HTML5 placeholder with js fallback.
911 *
912 * @param array $field
913 * @param array $add_html
914 *
915 * @return void
916 */
917 private static function add_placeholder_to_input( $field, &$add_html ) {
918 if ( FrmFieldsHelper::is_placeholder_field_type( $field['type'] ) ) {
919 $add_html['placeholder'] = 'placeholder="' . esc_attr( $field['placeholder'] ) . '"';
920 }
921 }
922
923 /**
924 * @param array $field
925 * @param array $add_html
926 *
927 * @return void
928 */
929 private static function add_frmval_to_input( $field, &$add_html ) {
930 // phpcs:ignore Universal.Operators.StrictComparisons
931 if ( $field['placeholder'] != '' ) {
932 $add_html['data-frmval'] = 'data-frmval="' . esc_attr( $field['placeholder'] ) . '"';
933
934 if ( 'select' === $field['type'] ) {
935 $add_html['data-frmplaceholder'] = 'data-frmplaceholder="' . esc_attr( $field['placeholder'] ) . '"';
936 }
937 }
938
939 // phpcs:ignore Universal.Operators.StrictComparisons
940 if ( $field['default_value'] != '' ) {
941 $add_html['data-frmval'] = 'data-frmval="' . esc_attr( $field['default_value'] ) . '"';
942 }
943 }
944
945 /**
946 * @param array $field
947 * @param array $add_html
948 *
949 * @return void
950 */
951 private static function add_validation_messages( $field, array &$add_html ) {
952 $field_validation_messages_status = self::get_validation_data_attribute_visibility_info( $field );
953
954 if ( FrmField::is_required( $field ) && ! empty( $field_validation_messages_status['data-reqmsg'] ) ) {
955 $required_message = FrmFieldsHelper::get_error_msg( $field, 'blank' );
956 $add_html['data-reqmsg'] = 'data-reqmsg="' . esc_attr( $required_message ) . '"';
957 self::maybe_add_html_required( $field, $add_html );
958 }
959
960 if ( ! FrmField::is_option_empty( $field, 'invalid' ) && ! empty( $field_validation_messages_status['data-invmsg'] ) ) {
961 $invalid_message = FrmFieldsHelper::get_error_msg( $field, 'invalid' );
962 $add_html['data-invmsg'] = 'data-invmsg="' . esc_attr( $invalid_message ) . '"';
963 }
964
965 if ( ! empty( $add_html['data-reqmsg'] ) || ! empty( $add_html['data-invmsg'] ) ) {
966 self::maybe_add_error_html_for_js_validation( $field, $add_html );
967 }
968 }
969
970 /**
971 * Returns an array that contains field validation messages status.
972 *
973 * @since 6.9
974 *
975 * @param array|object $field
976 *
977 * @return array
978 */
979 private static function get_validation_data_attribute_visibility_info( $field ) {
980 if ( FrmField::get_field_type( $field ) === 'hidden' ) {
981 $field_validation_data_attributes = array(
982 'data-invmsg' => false,
983 'data-reqmsg' => false,
984 );
985 } else {
986 $field_validation_data_attributes = array(
987 'data-invmsg' => true,
988 'data-reqmsg' => true,
989 );
990 }
991
992 /**
993 * Allows controlling which field validation messages would be included in the field html.
994 *
995 * @since 6.9
996 *
997 * @param array $field_validation_messages_status
998 * @param array|object $field
999 */
1000 return apply_filters( 'frm_field_validation_include_data_attributes', $field_validation_data_attributes, $field );
1001 }
1002
1003 /**
1004 * @since 5.0.03
1005 *
1006 * @param array $field
1007 * @param array $add_html
1008 *
1009 * @return void
1010 */
1011 private static function maybe_add_error_html_for_js_validation( $field, array &$add_html ) {
1012 $form = self::get_form_for_js_validation( $field );
1013
1014 if ( false === $form ) {
1015 return;
1016 }
1017
1018 $error_body = self::pull_custom_error_body_from_custom_html( $form, $field );
1019
1020 if ( false === $error_body ) {
1021 return;
1022 }
1023
1024 $error_body = urlencode( $error_body );
1025 $add_html['data-error-html'] = 'data-error-html="' . esc_attr( $error_body ) . '"';
1026 }
1027
1028 /**
1029 * @since 5.0.03
1030 *
1031 * @param array $field
1032 *
1033 * @return false|stdClass false if there is no form object found with JS validation active.
1034 */
1035 private static function get_form_for_js_validation( $field ) {
1036 global $frm_vars;
1037
1038 if ( ! empty( $frm_vars['js_validate_forms'] ) ) {
1039 if ( isset( $frm_vars['js_validate_forms'][ $field['form_id'] ] ) ) {
1040 return $frm_vars['js_validate_forms'][ $field['form_id'] ];
1041 }
1042
1043 if ( ! empty( $field['parent_form_id'] ) && isset( $frm_vars['js_validate_forms'][ $field['parent_form_id'] ] ) ) {
1044 return $frm_vars['js_validate_forms'][ $field['parent_form_id'] ];
1045 }
1046 }
1047
1048 return false;
1049 }
1050
1051 /**
1052 * @param stdClass $form
1053 * @param array $field
1054 * @param array $errors
1055 *
1056 * @return false|string
1057 */
1058 public static function pull_custom_error_body_from_custom_html( $form, $field, $errors = array() ) {
1059 if ( empty( $field['custom_html'] ) ) {
1060 return false;
1061 }
1062
1063 $custom_html = $field['custom_html'];
1064 $custom_html = apply_filters( 'frm_before_replace_shortcodes', $custom_html, $field, $errors, $form );
1065 $start = strpos( $custom_html, '[if error]' );
1066
1067 if ( false === $start ) {
1068 return false;
1069 }
1070
1071 $end = strpos( $custom_html, '[/if error]' );
1072
1073 if ( false === $end || $end < $start ) {
1074 return false;
1075 }
1076
1077 $error_body = substr( $custom_html, $start + 10, $end - $start - 10 );
1078 $default_html = array(
1079 '<div class="frm_error" id="frm_error_field_[key]">[error]</div>',
1080 '<div class="frm_error" role="alert" id="frm_error_field_[key]">[error]</div>',
1081 '<div class="frm_error">[error]</div>',
1082 '<div class="frm_error" role="alert">[error]</div>',
1083 );
1084
1085 return in_array( $error_body, $default_html, true ) ? false : $error_body;
1086 }
1087
1088 /**
1089 * If 'required' is added to a conditionally hidden field, the form won't
1090 * submit in many browsers. Check to make sure the javascript to conditionally
1091 * remove it is present if needed.
1092 *
1093 * @since 3.06.01
1094 *
1095 * @param array $field
1096 * @param array $add_html
1097 *
1098 * @return void
1099 */
1100 private static function maybe_add_html_required( $field, array &$add_html ) {
1101 // phpcs:disable Generic.WhiteSpace.ScopeIndent
1102 $excluded_field_types =
1103 FrmField::is_radio( $field ) ||
1104 FrmField::is_checkbox( $field ) ||
1105 FrmField::is_field_type( $field, 'file' ) ||
1106 FrmField::is_field_type( $field, 'nps' ) ||
1107 FrmField::is_field_type( $field, 'scale' );
1108 // phpcs:enable Generic.WhiteSpace.ScopeIndent
1109
1110 if ( $excluded_field_types ) {
1111 return;
1112 }
1113
1114 $add_html['aria-required'] = 'aria-required="true"';
1115 }
1116
1117 /**
1118 * @param array $field
1119 * @param array $add_html
1120 *
1121 * @return void
1122 */
1123 private static function add_shortcodes_to_html( $field, array &$add_html ) {
1124 if ( FrmField::is_option_empty( $field, 'shortcodes' ) ) {
1125 return;
1126 }
1127
1128 if ( ! empty( $field['autocomplete'] ) ) {
1129 unset( $field['shortcodes']['autocomplete'] );
1130 }
1131
1132 foreach ( $field['shortcodes'] as $k => $v ) {
1133 if ( isset( $field['subfield_name'] ) && str_starts_with( $k, 'aria-invalid' ) ) {
1134 $subfield_name = $field['subfield_name'];
1135
1136 if ( ! isset( $field['shortcodes'][ 'aria-invalid-' . $subfield_name ] ) ) {
1137 continue;
1138 }
1139 // Change the key to the correct aria-invalid value so that $add_html is set correctly for the current subfield of a combo field.
1140 $k = 'aria-invalid';
1141 $v = $field['shortcodes'][ 'aria-invalid-' . $subfield_name ];
1142 unset( $field['shortcodes'][ 'aria-invalid-' . $subfield_name ] );
1143 }
1144
1145 if ( 'opt' === $k || ! self::should_allow_input_attribute( $k ) ) {
1146 continue;
1147 }
1148
1149 if ( is_numeric( $k ) && str_contains( $v, '=' ) ) {
1150 $add_html[] = $v;
1151 } elseif ( ! empty( $k ) && isset( $add_html[ $k ] ) ) {
1152 $add_html[ $k ] = str_replace( $k . '="', $k . '="' . $v, $add_html[ $k ] );
1153 } else {
1154 $add_html[ $k ] = $k . '="' . esc_attr( $v ) . '"';
1155 }
1156
1157 unset( $k, $v );
1158 }//end foreach
1159 }
1160
1161 /**
1162 * Disallow possibly unsafe attributees (that trigger JavaScript) when unasfe HTML is not allowed.
1163 *
1164 * @since 6.11.2
1165 *
1166 * @param string $key The option key.
1167 *
1168 * @return bool
1169 */
1170 private static function should_allow_input_attribute( $key ) {
1171 if ( ! FrmAppHelper::should_never_allow_unfiltered_html() ) {
1172 return true;
1173 }
1174 return FrmAppHelper::input_key_is_safe( $key );
1175 }
1176
1177 /**
1178 * Add pattern attribute.
1179 *
1180 * @since 3.0
1181 *
1182 * @param array $field
1183 * @param array $add_html
1184 *
1185 * @return void
1186 */
1187 private static function add_pattern_attribute( $field, array &$add_html ) {
1188 $format_value = FrmField::get_option( $field, 'format' );
1189 $has_format = $format_value && ! FrmCurrencyHelper::is_currency_format( $format_value );
1190 $format_field = FrmField::is_field_type( $field, 'text' );
1191
1192 if ( $field['type'] !== 'phone' && ( ! $has_format || ! $format_field ) ) {
1193 return;
1194 }
1195
1196 $format = FrmEntryValidate::phone_format( $field );
1197 $format = substr( $format, 2, - 1 );
1198
1199 $add_html['pattern'] = 'pattern="' . esc_attr( $format ) . '"';
1200 }
1201
1202 /**
1203 * Add product attributes to fields in multi-paged forms.
1204 *
1205 * @param array $field
1206 * @param array $add_html
1207 *
1208 * @return void
1209 */
1210 private static function add_currency_field_attributes( $field, &$add_html ) {
1211 $type = $field['original_type'] ?? $field['type'];
1212 $is_product_field = in_array( $type, array( 'total', 'quantity', 'product' ), true );
1213
1214 if ( ! $is_product_field ) {
1215 return;
1216 }
1217
1218 if ( $type === 'total' ) {
1219 $add_html['data-frmtotal'] = 'data-frmtotal';
1220 } elseif ( $type === 'quantity' ) {
1221 $product_field = FrmField::get_option( $field, 'product_field' );
1222 $add_html['data-frmproduct'] = 'data-frmproduct="' . esc_attr( json_encode( $product_field ) ) . '"';
1223 } elseif ( $type === 'product' && 'hidden' === $field['type'] ) {
1224 // We want to do this only for fields that are hidden because it's
1225 // not their page, hence the check : 'hidden' === $field['type'].
1226 $price = empty( $field['value'] ) ? 0 : self::get_product_price( $field );
1227
1228 $add_html['data-frmprice'] = 'data-frmprice="' . esc_attr( $price ) . '"';
1229 }
1230 }
1231
1232 /**
1233 * @param array $field
1234 */
1235 private static function get_product_price( $field ) {
1236 if ( is_array( $field['value'] ) ) {
1237 // '' is unlikely though, let's just do it to prevent warnings
1238 $value = isset( $field['opt_key'] ) && isset( $field['value'][ $field['opt_key'] ] )
1239 ? $field['value'][ $field['opt_key'] ]
1240 : '';
1241 } else {
1242 $value = $field['value'];
1243 }
1244
1245 $field_obj = FrmFieldFactory::get_field_object( $field['id'] );
1246
1247 if ( method_exists( $field_obj, 'get_posted_price' ) ) {
1248 return $field_obj->get_posted_price( $value );
1249 }
1250
1251 return $value;
1252 }
1253
1254 /**
1255 * @param mixed $opt
1256 * @param mixed $opt_key
1257 * @param array|object $field
1258 *
1259 * @return mixed
1260 */
1261 public static function check_value( $opt, $opt_key, $field ) {
1262 if ( is_array( $opt ) ) {
1263 if ( FrmField::is_option_true( $field, 'separate_value' ) ) {
1264 $opt = $opt['value'] ?? $opt['label'] ?? reset( $opt );
1265 } else {
1266 $opt = $opt['label'] ?? reset( $opt );
1267 }
1268 }
1269
1270 return $opt;
1271 }
1272
1273 /**
1274 * @param mixed $opt
1275 *
1276 * @return mixed
1277 */
1278 public static function check_label( $opt ) {
1279 return is_array( $opt ) ? ( $opt['label'] ?? reset( $opt ) ) : $opt;
1280 }
1281 }
1282