PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.29
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.29
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / classes / controllers / FrmFieldsController.php

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

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