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

1,081 lines 29.3 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 // phpcs:ignore Universal.Operators.StrictComparisons
298 if ( ! empty( $field['field_options']['other'] ) ) {
299 $other_array = array();
300
301 foreach ( $field['options'] as $opt_key => $opt ) {
302 if ( FrmFieldsHelper::is_other_opt( $opt_key ) ) {
303 $other_array[ $opt_key ] = $opt;
304 }
305 unset( $opt_key, $opt );
306 }
307
308 if ( $other_array ) {
309 $opts = array_merge( $opts, $other_array );
310 }
311 }
312
313 $field['options'] = $opts;
314
315 FrmFieldsHelper::show_single_option( $field );
316
317 wp_die();
318 }
319
320 /**
321 * @since 4.0
322 *
323 * @param array $atts - Includes field array, field_obj, display array, values array.
324 *
325 * @return void
326 */
327 public static function load_single_field_settings( $atts ) {
328 $field = $atts['field'];
329 $field_obj = $atts['field_obj'];
330 $values = $atts['values'];
331 $display = $atts['display'];
332 unset( $atts );
333
334 if ( ! isset( $field['unique'] ) ) {
335 $field['unique'] = false;
336 }
337
338 if ( ! isset( $field['read_only'] ) ) {
339 $field['read_only'] = false;
340 }
341
342 $field_selection_data = self::maybe_define_field_selection_data();
343 $all_field_types = $field_selection_data->all_field_types;
344 $disabled_fields = $field_selection_data->disabled_fields;
345 $frm_settings = FrmAppHelper::get_settings();
346 $field_types = FrmFieldTypeOptionData::get_field_types( $field['type'] );
347
348 if ( ! isset( $all_field_types[ $field['type'] ] ) ) {
349 // Add fallback for an add-on field type that has been deactivated.
350 $all_field_types[ $field['type'] ] = array(
351 'name' => ucfirst( $field['type'] ),
352 'icon' => 'frmfont frm_pencil_icon',
353 );
354 } elseif ( ! is_array( $all_field_types[ $field['type'] ] ) ) {
355 // Fallback for fields added in a more basic way.
356 FrmFormsHelper::prepare_field_type( $all_field_types[ $field['type'] ] );
357 }
358
359 $type_name = $all_field_types[ $field['type'] ]['name'];
360
361 if ( $field['type'] === 'divider' && FrmField::is_option_true( $field, 'repeat' ) ) {
362 $type_name = $all_field_types['divider|repeat']['name'];
363 }
364
365 if ( $display['default'] ) {
366 $default_value_types = self::default_value_types( $field, compact( 'display' ) );
367 }
368
369 if ( $display['clear_on_focus'] && is_array( $field['placeholder'] ) ) {
370 $field['placeholder'] = implode( ', ', $field['placeholder'] );
371 }
372
373 include FrmAppHelper::plugin_path() . '/classes/views/frm-fields/back-end/settings.php';
374 }
375
376 /**
377 * @since 6.9.1
378 *
379 * @return FrmFieldSelectionData
380 */
381 private static function maybe_define_field_selection_data() {
382 if ( ! isset( self::$field_selection_data ) ) {
383 self::$field_selection_data = new FrmFieldSelectionData();
384 }
385 return self::$field_selection_data;
386 }
387
388 /**
389 * Get the list of default value types that can be toggled in the builder.
390 *
391 * @since 4.0
392 *
393 * @param array $field
394 * @param array $atts
395 *
396 * @return array
397 */
398 private static function default_value_types( $field, $atts ) {
399 $types = array(
400 'default_value' => array(
401 'class' => '',
402 'icon' => 'frmfont frm_text2_icon',
403 'title' => __( 'Default Value', 'formidable' ),
404 'data' => array(
405 'frmshow' => '#default-value-for-',
406 ),
407 ),
408 'calc' => array(
409 'class' => 'frm_show_upgrade frm_noallow',
410 'title' => __( 'Calculate Value', 'formidable' ),
411 'icon' => 'frmfont frm_calculator_icon',
412 'data' => array(
413 'medium' => 'calculations',
414 'upgrade' => __( 'Calculator forms', 'formidable' ),
415 ),
416 'tooltip' => __( 'Automatically calculate the value of this field based on values from other fields.', 'formidable' ),
417 ),
418 'get_values_field' => array(
419 'class' => 'frm_show_upgrade frm_noallow',
420 'title' => __( 'Lookup', 'formidable' ),
421 'icon' => 'frmfont frm_search_icon',
422 'data' => array(
423 'medium' => 'lookup',
424 'upgrade' => __( 'Lookup fields', 'formidable' ),
425 ),
426 'tooltip' => __( 'Dynamically retrieve the value of this field from a lookup field.', 'formidable' ),
427 ),
428 );
429
430 $types = apply_filters( 'frm_default_value_types', $types, $atts );
431
432 // Set active class.
433 $settings = array_keys( $types );
434 $active = 'default_value';
435
436 if ( FrmAppHelper::pro_is_connected() ) {
437 foreach ( $settings as $type ) {
438 if ( ! empty( $field[ $type ] ) ) {
439 $active = $type;
440 }
441 }
442 }
443
444 $types[ $active ]['class'] .= ' current';
445 $types[ $active ]['current'] = true;
446
447 return $types;
448 }
449
450 /**
451 * @param string $type
452 *
453 * @return string
454 */
455 public static function change_type( $type ) {
456 $type_switch = array(
457 'scale' => 'radio',
458 'star' => 'radio',
459 '10radio' => 'radio',
460 'rte' => 'textarea',
461 'website' => 'url',
462 'image' => 'url',
463 );
464
465 if ( isset( $type_switch[ $type ] ) ) {
466 $type = $type_switch[ $type ];
467 }
468
469 $pro_fields = FrmField::pro_field_selection();
470 // We want to keep credit_card types as credit card types for Stripe Lite.
471 // The credit_card key is set for backward compatibility.
472 unset( $pro_fields['credit_card'] );
473
474 return array_key_exists( $type, $pro_fields ) ? 'text' : $type;
475 }
476
477 /**
478 * @param array $settings
479 * @param FrmFieldType|null $field_info
480 *
481 * @return array
482 */
483 public static function display_field_options( $settings, $field_info = null ) {
484 if ( $field_info ) {
485 $settings = $field_info->display_field_settings();
486
487 // Field appears to be protected but there is a __get function in FrmFieldType that makes this public.
488 $settings['field_data'] = $field_info->field;
489 }
490
491 return apply_filters( 'frm_display_field_options', $settings );
492 }
493
494 /**
495 * Display the format option
496 *
497 * @since 3.0
498 *
499 * @param array $field Field data.
500 * @param bool $is_hidden Whether the format option should be hidden.
501 *
502 * @return void
503 */
504 public static function show_format_option( $field, $is_hidden = false ) {
505 $attributes = array(
506 'class' => 'frm-has-modal',
507 'id' => 'frm-field-format-custom-' . $field['id'],
508 );
509
510 if ( $is_hidden ) {
511 $attributes['class'] .= ' frm_hidden';
512 }
513
514 include FrmAppHelper::plugin_path() . '/classes/views/frm-fields/back-end/value-format.php';
515 }
516
517 /**
518 * @param array $field
519 * @param bool $echo
520 *
521 * @return string
522 */
523 public static function input_html( $field, $echo = true ) {
524 $class = array();
525 self::add_input_classes( $field, $class );
526
527 $add_html = array();
528 self::add_html_size( $field, $add_html );
529 self::add_html_length( $field, $add_html );
530 self::add_html_placeholder( $field, $add_html, $class );
531 self::add_validation_messages( $field, $add_html );
532
533 $class = apply_filters( 'frm_field_classes', implode( ' ', $class ), $field );
534
535 FrmFormsHelper::add_html_attr( $class, 'class', $add_html );
536
537 self::add_shortcodes_to_html( $field, $add_html );
538 self::add_pattern_attribute( $field, $add_html );
539
540 $add_html = apply_filters( 'frm_field_extra_html', $add_html, $field );
541 $add_html = ' ' . implode( ' ', $add_html ) . ' ';
542
543 if ( $echo ) {
544 echo $add_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
545 }
546
547 return $add_html;
548 }
549
550 /**
551 * @param array $field
552 * @param array $class
553 *
554 * @return void
555 */
556 private static function add_input_classes( $field, array &$class ) {
557 if ( ! empty( $field['input_class'] ) ) {
558 $class[] = $field['input_class'];
559 }
560
561 if ( $field['type'] === 'hidden' || $field['type'] === 'user_id' ) {
562 return;
563 }
564
565 if ( isset( $field['size'] ) && $field['size'] > 0 ) {
566 $class[] = 'auto_width';
567 }
568 }
569
570 /**
571 * @param array $field
572 * @param array $add_html
573 *
574 * @return void
575 */
576 private static function add_html_size( $field, array &$add_html ) {
577 $size_fields = array(
578 'select',
579 'data',
580 'time',
581 'hidden',
582 'file',
583 'lookup',
584 );
585
586 if ( ! isset( $field['size'] ) || $field['size'] <= 0 || in_array( $field['type'], $size_fields, true ) ) {
587 return;
588 }
589
590 if ( FrmAppHelper::is_admin_page( 'formidable' ) ) {
591 return;
592 }
593
594 if ( is_numeric( $field['size'] ) ) {
595 $field['size'] .= 'px';
596 }
597
598 $important = apply_filters( 'frm_use_important_width', 1, $field );
599 // Note: This inline styling must stay since we cannot realistically set a class for every possible field size.
600 $add_html['style'] = 'style="width:' . esc_attr( $field['size'] ) . ( $important ? ' !important' : '' ) . '"';
601
602 self::add_html_cols( $field, $add_html );
603 }
604
605 /**
606 * @param array $field
607 * @param array $add_html
608 *
609 * @return void
610 */
611 private static function add_html_cols( $field, array &$add_html ) {
612 if ( ! in_array( $field['type'], array( 'textarea', 'rte' ), true ) ) {
613 return;
614 }
615
616 // Convert to cols for textareas.
617 $calc = array(
618 '' => 9,
619 'px' => 9,
620 'rem' => 0.444,
621 'em' => 0.544,
622 );
623
624 // Include "col" for valid html
625 $unit = trim( preg_replace( '/[0-9]+/', '', $field['size'] ) );
626
627 if ( ! isset( $calc[ $unit ] ) ) {
628 return;
629 }
630
631 $size = (float) str_replace( $unit, '', $field['size'] ) / $calc[ $unit ];
632 $add_html['cols'] = 'cols="' . absint( $size ) . '"';
633 }
634
635 /**
636 * @param array $field
637 * @param array $add_html
638 *
639 * @return void
640 */
641 private static function add_html_length( $field, array &$add_html ) {
642 // Check for max setting and if this field accepts maxlength.
643 $fields = array(
644 'textarea',
645 'rte',
646 'hidden',
647 'file',
648 );
649
650 if ( FrmField::is_option_empty( $field, 'max' ) || in_array( $field['type'], $fields, true ) ) {
651 return;
652 }
653
654 if ( FrmAppHelper::is_admin_page( 'formidable' ) ) {
655 // Don't load on form builder page.
656 return;
657 }
658
659 $add_html['maxlength'] = 'maxlength="' . esc_attr( $field['max'] ) . '"';
660 }
661
662 /**
663 * @param array $field
664 * @param array $add_html
665 * @param array $class
666 *
667 * @return void
668 */
669 private static function add_html_placeholder( $field, array &$add_html, array &$class ) {
670 // phpcs:ignore Universal.Operators.StrictComparisons
671 if ( $field['default_value'] != '' ) {
672 if ( is_array( $field['default_value'] ) ) {
673 $add_html['data-frmval'] = 'data-frmval="' . esc_attr( json_encode( $field['default_value'] ) ) . '"';
674 } else {
675 self::add_frmval_to_input( $field, $add_html );
676 }
677 }
678
679 $field['placeholder'] = self::prepare_placeholder( $field );
680
681 // phpcs:ignore Universal.Operators.StrictComparisons
682 if ( $field['placeholder'] == '' || is_array( $field['placeholder'] ) ) {
683 // Don't include a json placeholder
684 return;
685 }
686
687 self::add_placeholder_to_input( $field, $add_html );
688 }
689
690 /**
691 * Prepares field placeholder.
692 *
693 * @since 5.4 This doesn't call `FrmFieldsController::get_default_value_from_name()` anymore.
694 *
695 * @param array $field Field array.
696 *
697 * @return string
698 */
699 private static function prepare_placeholder( $field ) {
700 return $field['placeholder'] ?? '';
701 }
702
703 /**
704 * If the label position is "inside",
705 * get the label to use as the placeholder
706 *
707 * @since 2.05
708 * @since 5.4 Remove the logic code for "inside" label position.
709 *
710 * @param array $field
711 *
712 * @return string
713 */
714 public static function get_default_value_from_name( $field ) {
715 return '';
716 }
717
718 /**
719 * Maybe add a blank placeholder option before any options
720 * in a dropdown.
721 *
722 * @since 4.04
723 *
724 * @param array|object $field
725 *
726 * @return bool True if placeholder was added.
727 */
728 public static function add_placeholder_to_select( $field ) {
729 $placeholder = FrmField::get_option( $field, 'placeholder' );
730
731 if ( ! $placeholder ) {
732 $placeholder = self::get_default_value_from_name( $field );
733 }
734
735 $use_placeholder = $placeholder;
736 $autocomplete = FrmField::get_option( $field, 'autocom' );
737
738 if ( $autocomplete ) {
739 $use_chosen = ! is_callable( 'FrmProAppHelper::use_chosen_js' ) || FrmProAppHelper::use_chosen_js();
740
741 if ( $use_chosen ) {
742 $use_placeholder = '';
743 }
744 }
745
746 if ( $placeholder !== '' ) {
747 $placeholder_attributes = array(
748 'class' => 'frm-select-placeholder',
749 'value' => '',
750 'data-placeholder' => 'true',
751 );
752
753 FrmHtmlHelper::echo_dropdown_option( $use_placeholder, false, $placeholder_attributes );
754 return true;
755 }
756
757 return false;
758 }
759
760 /**
761 * Use HTML5 placeholder with js fallback.
762 *
763 * @param array $field
764 * @param array $add_html
765 *
766 * @return void
767 */
768 private static function add_placeholder_to_input( $field, &$add_html ) {
769 if ( FrmFieldsHelper::is_placeholder_field_type( $field['type'] ) ) {
770 $add_html['placeholder'] = 'placeholder="' . esc_attr( $field['placeholder'] ) . '"';
771 }
772 }
773
774 /**
775 * @param array $field
776 * @param array $add_html
777 *
778 * @return void
779 */
780 private static function add_frmval_to_input( $field, &$add_html ) {
781 // phpcs:ignore Universal.Operators.StrictComparisons
782 if ( $field['placeholder'] != '' ) {
783 $add_html['data-frmval'] = 'data-frmval="' . esc_attr( $field['placeholder'] ) . '"';
784
785 if ( 'select' === $field['type'] ) {
786 $add_html['data-frmplaceholder'] = 'data-frmplaceholder="' . esc_attr( $field['placeholder'] ) . '"';
787 }
788 }
789
790 // phpcs:ignore Universal.Operators.StrictComparisons
791 if ( $field['default_value'] != '' ) {
792 $add_html['data-frmval'] = 'data-frmval="' . esc_attr( $field['default_value'] ) . '"';
793 }
794 }
795
796 /**
797 * @param array $field
798 * @param array $add_html
799 *
800 * @return void
801 */
802 private static function add_validation_messages( $field, array &$add_html ) {
803 $field_validation_messages_status = self::get_validation_data_attribute_visibility_info( $field );
804
805 if ( FrmField::is_required( $field ) && ! empty( $field_validation_messages_status['data-reqmsg'] ) ) {
806 $required_message = FrmFieldsHelper::get_error_msg( $field, 'blank' );
807 $add_html['data-reqmsg'] = 'data-reqmsg="' . esc_attr( $required_message ) . '"';
808 self::maybe_add_html_required( $field, $add_html );
809 }
810
811 if ( ! FrmField::is_option_empty( $field, 'invalid' ) && ! empty( $field_validation_messages_status['data-invmsg'] ) ) {
812 $invalid_message = FrmFieldsHelper::get_error_msg( $field, 'invalid' );
813 $add_html['data-invmsg'] = 'data-invmsg="' . esc_attr( $invalid_message ) . '"';
814 }
815
816 if ( ! empty( $add_html['data-reqmsg'] ) || ! empty( $add_html['data-invmsg'] ) ) {
817 self::maybe_add_error_html_for_js_validation( $field, $add_html );
818 }
819 }
820
821 /**
822 * Returns an array that contains field validation messages status.
823 *
824 * @since 6.9
825 *
826 * @param array|object $field
827 *
828 * @return array
829 */
830 private static function get_validation_data_attribute_visibility_info( $field ) {
831 if ( FrmField::get_field_type( $field ) === 'hidden' ) {
832 $field_validation_data_attributes = array(
833 'data-invmsg' => false,
834 'data-reqmsg' => false,
835 );
836 } else {
837 $field_validation_data_attributes = array(
838 'data-invmsg' => true,
839 'data-reqmsg' => true,
840 );
841 }
842
843 /**
844 * Allows controlling which field validation messages would be included in the field html.
845 *
846 * @since 6.9
847 *
848 * @param array $field_validation_messages_status
849 * @param array|object $field
850 */
851 return apply_filters( 'frm_field_validation_include_data_attributes', $field_validation_data_attributes, $field );
852 }
853
854 /**
855 * @since 5.0.03
856 *
857 * @param array $field
858 * @param array $add_html
859 *
860 * @return void
861 */
862 private static function maybe_add_error_html_for_js_validation( $field, array &$add_html ) {
863 $form = self::get_form_for_js_validation( $field );
864
865 if ( false === $form ) {
866 return;
867 }
868
869 $error_body = self::pull_custom_error_body_from_custom_html( $form, $field );
870
871 if ( false === $error_body ) {
872 return;
873 }
874
875 $error_body = urlencode( $error_body );
876 $add_html['data-error-html'] = 'data-error-html="' . esc_attr( $error_body ) . '"';
877 }
878
879 /**
880 * @since 5.0.03
881 *
882 * @param array $field
883 *
884 * @return false|stdClass false if there is no form object found with JS validation active.
885 */
886 private static function get_form_for_js_validation( $field ) {
887 global $frm_vars;
888
889 if ( ! empty( $frm_vars['js_validate_forms'] ) ) {
890 if ( isset( $frm_vars['js_validate_forms'][ $field['form_id'] ] ) ) {
891 return $frm_vars['js_validate_forms'][ $field['form_id'] ];
892 }
893
894 if ( ! empty( $field['parent_form_id'] ) && isset( $frm_vars['js_validate_forms'][ $field['parent_form_id'] ] ) ) {
895 return $frm_vars['js_validate_forms'][ $field['parent_form_id'] ];
896 }
897 }
898
899 return false;
900 }
901
902 /**
903 * @param stdClass $form
904 * @param array $field
905 * @param array $errors
906 *
907 * @return false|string
908 */
909 public static function pull_custom_error_body_from_custom_html( $form, $field, $errors = array() ) {
910 if ( empty( $field['custom_html'] ) ) {
911 return false;
912 }
913
914 $custom_html = $field['custom_html'];
915 $custom_html = apply_filters( 'frm_before_replace_shortcodes', $custom_html, $field, $errors, $form );
916 $start = strpos( $custom_html, '[if error]' );
917
918 if ( false === $start ) {
919 return false;
920 }
921
922 $end = strpos( $custom_html, '[/if error]' );
923
924 if ( false === $end || $end < $start ) {
925 return false;
926 }
927
928 $error_body = substr( $custom_html, $start + 10, $end - $start - 10 );
929 $default_html = array(
930 '<div class="frm_error" id="frm_error_field_[key]">[error]</div>',
931 '<div class="frm_error" role="alert" id="frm_error_field_[key]">[error]</div>',
932 '<div class="frm_error">[error]</div>',
933 '<div class="frm_error" role="alert">[error]</div>',
934 );
935
936 return in_array( $error_body, $default_html, true ) ? false : $error_body;
937 }
938
939 /**
940 * If 'required' is added to a conditionally hidden field, the form won't
941 * submit in many browsers. Check to make sure the javascript to conditionally
942 * remove it is present if needed.
943 *
944 * @since 3.06.01
945 *
946 * @param array $field
947 * @param array $add_html
948 *
949 * @return void
950 */
951 private static function maybe_add_html_required( $field, array &$add_html ) {
952 // phpcs:disable Generic.WhiteSpace.ScopeIndent
953 $excluded_field_types =
954 FrmField::is_radio( $field ) ||
955 FrmField::is_checkbox( $field ) ||
956 FrmField::is_field_type( $field, 'file' ) ||
957 FrmField::is_field_type( $field, 'nps' ) ||
958 FrmField::is_field_type( $field, 'scale' );
959 // phpcs:enable Generic.WhiteSpace.ScopeIndent
960
961 if ( $excluded_field_types ) {
962 return;
963 }
964
965 $add_html['aria-required'] = 'aria-required="true"';
966 }
967
968 /**
969 * @param array $field
970 * @param array $add_html
971 *
972 * @return void
973 */
974 private static function add_shortcodes_to_html( $field, array &$add_html ) {
975 if ( FrmField::is_option_empty( $field, 'shortcodes' ) ) {
976 return;
977 }
978
979 if ( ! empty( $field['autocomplete'] ) ) {
980 unset( $field['shortcodes']['autocomplete'] );
981 }
982
983 foreach ( $field['shortcodes'] as $k => $v ) {
984 if ( isset( $field['subfield_name'] ) && str_starts_with( $k, 'aria-invalid' ) ) {
985 $subfield_name = $field['subfield_name'];
986
987 if ( ! isset( $field['shortcodes'][ 'aria-invalid-' . $subfield_name ] ) ) {
988 continue;
989 }
990 // Change the key to the correct aria-invalid value so that $add_html is set correctly for the current subfield of a combo field.
991 $k = 'aria-invalid';
992 $v = $field['shortcodes'][ 'aria-invalid-' . $subfield_name ];
993 unset( $field['shortcodes'][ 'aria-invalid-' . $subfield_name ] );
994 }
995
996 if ( 'opt' === $k || ! self::should_allow_input_attribute( $k ) ) {
997 continue;
998 }
999
1000 if ( is_numeric( $k ) && str_contains( $v, '=' ) ) {
1001 $add_html[] = $v;
1002 } elseif ( ! empty( $k ) && isset( $add_html[ $k ] ) ) {
1003 $add_html[ $k ] = str_replace( $k . '="', $k . '="' . $v, $add_html[ $k ] );
1004 } else {
1005 $add_html[ $k ] = $k . '="' . esc_attr( $v ) . '"';
1006 }
1007
1008 unset( $k, $v );
1009 }//end foreach
1010 }
1011
1012 /**
1013 * Disallow possibly unsafe attributees (that trigger JavaScript) when unasfe HTML is not allowed.
1014 *
1015 * @since 6.11.2
1016 *
1017 * @param string $key The option key.
1018 *
1019 * @return bool
1020 */
1021 private static function should_allow_input_attribute( $key ) {
1022 if ( ! FrmAppHelper::should_never_allow_unfiltered_html() ) {
1023 return true;
1024 }
1025 return FrmAppHelper::input_key_is_safe( $key );
1026 }
1027
1028 /**
1029 * Add pattern attribute.
1030 *
1031 * @since 3.0
1032 *
1033 * @param array $field
1034 * @param array $add_html
1035 *
1036 * @return void
1037 */
1038 private static function add_pattern_attribute( $field, array &$add_html ) {
1039 $format_value = FrmField::get_option( $field, 'format' );
1040 $has_format = $format_value && ! FrmCurrencyHelper::is_currency_format( $format_value );
1041 $format_field = FrmField::is_field_type( $field, 'text' );
1042
1043 if ( $field['type'] !== 'phone' && ( ! $has_format || ! $format_field ) ) {
1044 return;
1045 }
1046
1047 $format = FrmEntryValidate::phone_format( $field );
1048 $format = substr( $format, 2, - 1 );
1049
1050 $add_html['pattern'] = 'pattern="' . esc_attr( $format ) . '"';
1051 }
1052
1053 /**
1054 * @param mixed $opt
1055 * @param mixed $opt_key
1056 * @param array|object $field
1057 *
1058 * @return mixed
1059 */
1060 public static function check_value( $opt, $opt_key, $field ) {
1061 if ( is_array( $opt ) ) {
1062 if ( FrmField::is_option_true( $field, 'separate_value' ) ) {
1063 $opt = $opt['value'] ?? $opt['label'] ?? reset( $opt );
1064 } else {
1065 $opt = $opt['label'] ?? reset( $opt );
1066 }
1067 }
1068
1069 return $opt;
1070 }
1071
1072 /**
1073 * @param mixed $opt
1074 *
1075 * @return mixed
1076 */
1077 public static function check_label( $opt ) {
1078 return is_array( $opt ) ? ( $opt['label'] ?? reset( $opt ) ) : $opt;
1079 }
1080 }
1081