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

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