PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.31
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.31
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 6.31, at classes/controllers/FrmFieldsController.php

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