form_id = isset( $_GET['form_id'] ) ? (int) $_GET['form_id'] : false; // phpcs:ignore WordPress.Security.NonceVerification
// Bootstrap.
$this->init();
// Temporary solution to get an object of the field class.
add_filter(
"wpforms_fields_get_field_object_{$this->type}",
function () {
return $this;
}
);
// Add fields tab.
add_filter( 'wpforms_builder_fields_buttons', array( $this, 'field_button' ), 15 );
// Field options tab.
add_action( "wpforms_builder_fields_options_{$this->type}", array( $this, 'field_options' ), 10 );
// Preview fields.
add_action( "wpforms_builder_fields_previews_{$this->type}", array( $this, 'field_preview' ), 10 );
// AJAX Add new field.
add_action( "wp_ajax_wpforms_new_field_{$this->type}", array( $this, 'field_new' ) );
// Display field input elements on front-end.
add_action( "wpforms_display_field_{$this->type}", array( $this, 'field_display' ), 10, 3 );
// Validation on submit.
add_action( "wpforms_process_validate_{$this->type}", array( $this, 'validate' ), 10, 3 );
// Format.
add_action( "wpforms_process_format_{$this->type}", array( $this, 'format' ), 10, 3 );
// Prefill.
add_filter( 'wpforms_field_properties', array( $this, 'field_prefill_value_property' ), 10, 3 );
// Change the choice's value while saving entries.
add_filter( 'wpforms_process_before_form_data', [ $this, 'field_fill_empty_choices' ] );
// Change field name for ajax error.
add_filter( 'wpforms_process_ajax_error_field_name', [ $this, 'ajax_error_field_name' ], 10, 4 );
}
/**
* All systems go. Used by subclasses. Required.
*
* @since 1.0.0
* @since 1.5.0 Converted to abstract method, as it's required for all fields.
*/
abstract public function init();
/**
* Prefill field value with either fallback or dynamic data.
* This needs to be public (although internal) to be used in WordPress hooks.
*
* @since 1.5.0
*
* @param array $properties Field properties.
* @param array $field Current field specific data.
* @param array $form_data Prepared form data/settings.
*
* @return array Modified field properties.
*/
public function field_prefill_value_property( $properties, $field, $form_data ) {
// Process only for current field.
if ( $this->type !== $field['type'] ) {
return $properties;
}
// Set the form data, so we can reuse it later, even on front-end.
$this->form_data = $form_data;
// Dynamic data.
if ( ! empty( $this->form_data['settings']['dynamic_population'] ) ) {
$properties = $this->field_prefill_value_property_dynamic( $properties, $field );
}
// Fallback data, rewrites dynamic because user-submitted data is more important.
$properties = $this->field_prefill_value_property_fallback( $properties, $field );
return $properties;
}
/**
* As we are processing user submitted data - ignore all admin-defined defaults.
* Preprocess choices-related fields only.
*
* @since 1.5.0
*
* @param array $field Field data and settings.
* @param array $properties Properties we are modifying.
*/
public function field_prefill_remove_choices_defaults( $field, &$properties ) {
// Skip this step on admin page.
if ( is_admin() && ! wpforms_is_admin_page( 'entries', 'edit' ) ) {
return;
}
if (
! empty( $field['dynamic_choices'] ) ||
! empty( $field['choices'] )
) {
array_walk_recursive(
$properties['inputs'],
function ( &$value, $key ) {
if ( 'default' === $key ) {
$value = false;
}
if ( 'wpforms-selected' === $value ) {
$value = '';
}
}
);
}
}
/**
* Whether current field can be populated dynamically.
*
* @since 1.5.0
*
* @param array $properties Field properties.
* @param array $field Current field specific data.
*
* @return bool
*/
public function is_dynamic_population_allowed( $properties, $field ) {
$allowed = true;
// Allow population on front-end only.
if ( is_admin() ) {
$allowed = false;
}
// For dynamic population we require $_GET.
if ( empty( $_GET ) ) { // phpcs:ignore
$allowed = false;
}
return apply_filters( 'wpforms_field_is_dynamic_population_allowed', $allowed, $properties, $field );
}
/**
* Prefill the field value with a dynamic value, that we get from $_GET.
* The pattern is: wpf4_12_primary, where:
* 4 - form_id,
* 12 - field_id,
* first - input key.
* As 'primary' is our default input key, "wpf4_12_primary" and "wpf4_12" are the same.
*
* @since 1.5.0
*
* @param array $properties Field properties.
* @param array $field Current field specific data.
*
* @return array Modified field properties.
*/
protected function field_prefill_value_property_dynamic( $properties, $field ) {
if ( ! $this->is_dynamic_population_allowed( $properties, $field ) ) {
return $properties;
}
// Iterate over each GET key, parse, and scrap data from there.
foreach ( $_GET as $key => $raw_value ) { // phpcs:ignore
preg_match( '/wpf(\d+)_(\d+)(.*)/i', $key, $matches );
if ( empty( $matches ) || ! is_array( $matches ) ) {
continue;
}
// Required.
$form_id = absint( $matches[1] );
$field_id = absint( $matches[2] );
$input = 'primary';
// Optional.
if ( ! empty( $matches[3] ) ) {
$input = sanitize_key( trim( $matches[3], '_' ) );
}
// Both form and field IDs should be the same as current form/field.
if (
(int) $this->form_data['id'] !== $form_id ||
(int) $field['id'] !== $field_id
) {
// Go to the next GET param.
continue;
}
if ( ! empty( $raw_value ) ) {
$this->field_prefill_remove_choices_defaults( $field, $properties );
}
/*
* Some fields (like checkboxes) support multiple selection.
* We do not support nested values, so omit them.
* Example: ?wpf771_19_wpforms[fields][19][address1]=test
* In this case:
* $input = wpforms
* $raw_value = [fields=>[]]
* $single_value = [19=>[]]
* There is no reliable way to clean those things out.
* So we will ignore the value altogether if it's an array.
* We support only single value numeric arrays, like these:
* ?wpf771_19[]=test1&wpf771_19[]=test2
* ?wpf771_19_value[]=test1&wpf771_19_value[]=test2
* ?wpf771_41_r3_c2[]=1&wpf771_41_r1_c4[]=1
*/
if ( is_array( $raw_value ) ) {
foreach ( $raw_value as $single_value ) {
$properties = $this->get_field_populated_single_property_value( $single_value, $input, $properties, $field );
}
} else {
$properties = $this->get_field_populated_single_property_value( $raw_value, $input, $properties, $field );
}
}
return $properties;
}
/**
* Public version of get_field_populated_single_property_value() to use by external classes.
*
* @since 1.6.0.1
*
* @param string $raw_value Value from a GET param, always a string.
* @param string $input Represent a subfield inside the field. May be empty.
* @param array $properties Field properties.
* @param array $field Current field specific data.
*
* @return array Modified field properties.
*/
public function get_field_populated_single_property_value_public( $raw_value, $input, $properties, $field ) {
return $this->get_field_populated_single_property_value( $raw_value, $input, $properties, $field );
}
/**
* Get the value, that is used to prefill via dynamic or fallback population.
* Based on field data and current properties.
*
* @since 1.5.0
*
* @param string $raw_value Value from a GET param, always a string.
* @param string $input Represent a subfield inside the field. May be empty.
* @param array $properties Field properties.
* @param array $field Current field specific data.
*
* @return array Modified field properties.
*/
protected function get_field_populated_single_property_value( $raw_value, $input, $properties, $field ) {
if ( ! is_string( $raw_value ) ) {
return $properties;
}
$get_value = stripslashes( sanitize_text_field( $raw_value ) );
// For fields that have dynamic choices we need to add extra logic.
if ( ! empty( $field['dynamic_choices'] ) ) {
$properties = $this->get_field_populated_single_property_value_dynamic_choices( $get_value, $properties );
} elseif ( ! empty( $field['choices'] ) && is_array( $field['choices'] ) ) {
$properties = $this->get_field_populated_single_property_value_normal_choices( $get_value, $properties, $field );
} else {
/*
* For other types of fields we need to check that
* the key is registered for the defined field in inputs array.
*/
if (
! empty( $input ) &&
isset( $properties['inputs'][ $input ] )
) {
$properties['inputs'][ $input ]['attr']['value'] = $get_value;
}
}
return $properties;
}
/**
* Get the value, that is used to prefill via dynamic or fallback population.
* Based on field data and current properties.
* Dynamic choices section.
*
* @since 1.6.0
*
* @param string $get_value Value from a GET param, always a string, sanitized, stripped slashes.
* @param array $properties Field properties.
*
* @return array Modified field properties.
*/
protected function get_field_populated_single_property_value_dynamic_choices( $get_value, $properties ) {
$default_key = null;
foreach ( $properties['inputs'] as $input_key => $input_arr ) {
// Dynamic choices support only integers in its values.
if ( absint( $get_value ) === $input_arr['attr']['value'] ) {
$default_key = $input_key;
// Stop iterating over choices.
break;
}
}
// Redefine default choice only if dynamic value has changed anything.
if ( null !== $default_key ) {
foreach ( $properties['inputs'] as $input_key => $choice_arr ) {
if ( $input_key === $default_key ) {
$properties['inputs'][ $input_key ]['default'] = true;
$properties['inputs'][ $input_key ]['container']['class'][] = 'wpforms-selected';
// Stop iterating over choices.
break;
}
}
}
return $properties;
}
/**
* Fill choices without labels.
*
* @since 1.6.2
*
* @param array $form_data Form data.
*
* @return array
*/
public function field_fill_empty_choices( $form_data ) {
if ( empty( $form_data['fields'] ) ) {
return $form_data;
}
// Set value for choices with the image only. Conditional logic doesn't work without value.
foreach ( $form_data['fields'] as $field_key => $field ) {
// Payment fields have their labels set up upfront.
if ( empty( $field['choices'] ) || ! in_array( $field['type'], [ 'radio', 'checkbox' ], true ) ) {
continue;
}
foreach ( $field['choices'] as $choice_id => $choice ) {
if ( ( isset( $choice['value'] ) && '' !== trim( $choice['value'] ) ) || empty( $choice['image'] ) ) {
continue;
}
/* translators: %d - choice number. */
$form_data['fields'][ $field_key ]['choices'][ $choice_id ]['value'] = sprintf( esc_html__( 'Choice %d', 'wpforms-lite' ), (int) $choice_id );
}
}
return $form_data;
}
/**
* Get the value, that is used to prefill via dynamic or fallback population.
* Based on field data and current properties.
* Normal choices section.
*
* @since 1.6.0
*
* @param string $get_value Value from a GET param, always a string, sanitized.
* @param array $properties Field properties.
* @param array $field Current field specific data.
*
* @return array Modified field properties.
*/
protected function get_field_populated_single_property_value_normal_choices( $get_value, $properties, $field ) {
$default_key = null;
// For fields that have normal choices we need to add extra logic.
foreach ( $field['choices'] as $choice_key => $choice_arr ) {
$choice_value_key = isset( $field['show_values'] ) ? 'value' : 'label';
if (
(
isset( $choice_arr[ $choice_value_key ] ) &&
strtoupper( sanitize_text_field( $choice_arr[ $choice_value_key ] ) ) === strtoupper( $get_value )
) ||
(
empty( $choice_arr[ $choice_value_key ] ) &&
/* translators: %d - choice number. */
$get_value === sprintf( esc_html__( 'Choice %d', 'wpforms-lite' ), (int) $choice_key )
)
) {
$default_key = $choice_key;
// Stop iterating over choices.
break;
}
}
// Redefine default choice only if population value has changed anything.
if ( null !== $default_key ) {
foreach ( $field['choices'] as $choice_key => $choice_arr ) {
if ( $choice_key === $default_key ) {
$properties['inputs'][ $choice_key ]['default'] = true;
$properties['inputs'][ $choice_key ]['container']['class'][] = 'wpforms-selected';
break;
}
}
}
return $properties;
}
/**
* Whether current field can be populated dynamically.
*
* @since 1.5.0
*
* @param array $properties Field properties.
* @param array $field Current field specific data.
*
* @return bool
*/
public function is_fallback_population_allowed( $properties, $field ) {
$allowed = true;
// Allow population on front-end only.
if ( is_admin() ) {
$allowed = false;
}
/*
* Commented out to allow partial fail for complex multi-inputs fields.
* Example: name field with first/last format and being required, filled out only first.
* On submit we will preserve those sub-inputs that are not empty and display an error for an empty.
*/
// Do not populate if there are errors for that field.
/*
$errors = wpforms()->process->errors;
if ( ! empty( $errors[ $this->form_data['id'] ][ $field['id'] ] ) ) {
$allowed = false;
}
*/
// Require form id being the same for submitted and currently rendered form.
if (
! empty( $_POST['wpforms']['id'] ) && // phpcs:ignore
(int) $_POST['wpforms']['id'] !== (int) $this->form_data['id'] // phpcs:ignore
) {
$allowed = false;
}
// Require $_POST of submitted field.
if ( empty( $_POST['wpforms']['fields'] ) ) { // phpcs:ignore
$allowed = false;
}
// Require field (processed and rendered) being the same.
if ( ! isset( $_POST['wpforms']['fields'][ $field['id'] ] ) ) { // phpcs:ignore
$allowed = false;
}
return apply_filters( 'wpforms_field_is_fallback_population_allowed', $allowed, $properties, $field );
}
/**
* Prefill the field value with a fallback value from form submission (in case of JS validation failed), that we get from $_POST.
*
* @since 1.5.0
*
* @param array $properties Field properties.
* @param array $field Current field specific data.
*
* @return array Modified field properties.
*/
protected function field_prefill_value_property_fallback( $properties, $field ) {
if ( ! $this->is_fallback_population_allowed( $properties, $field ) ) {
return $properties;
}
if ( empty( $_POST['wpforms']['fields'] ) || ! is_array( $_POST['wpforms']['fields'] ) ) { // phpcs:ignore
return $properties;
}
// We got user submitted raw data (not processed, will be done later).
$raw_value = $_POST['wpforms']['fields'][ $field['id'] ]; // phpcs:ignore
$input = 'primary';
if ( ! empty( $raw_value ) ) {
$this->field_prefill_remove_choices_defaults( $field, $properties );
}
/*
* For this particular field this value may be either array or a string.
* In array - this is a complex field, like address.
* The key in array will be a sub-input (address1, state), and its appropriate value.
*/
if ( is_array( $raw_value ) ) {
foreach ( $raw_value as $input => $single_value ) {
$properties = $this->get_field_populated_single_property_value( $single_value, sanitize_key( $input ), $properties, $field );
}
} else {
$properties = $this->get_field_populated_single_property_value( $raw_value, sanitize_key( $input ), $properties, $field );
}
return $properties;
}
/**
* Create the button for the 'Add Fields' tab, inside the form editor.
*
* @since 1.0.0
*
* @param array $fields List of form fields with their data.
*
* @return array
*/
public function field_button( $fields ) {
// Add field information to fields array.
$fields[ $this->group ]['fields'][] = array(
'order' => $this->order,
'name' => $this->name,
'type' => $this->type,
'icon' => $this->icon,
);
// Wipe hands clean.
return $fields;
}
/**
* Create the field options panel. Used by subclasses.
*
* @since 1.0.0
* @since 1.5.0 Converted to abstract method, as it's required for all fields.
*
* @param array $field Field data and settings.
*/
abstract public function field_options( $field );
/**
* Create the field preview. Used by subclasses.
*
* @since 1.0.0
* @since 1.5.0 Converted to abstract method, as it's required for all fields.
*
* @param array $field Field data and settings.
*/
abstract public function field_preview( $field );
/**
* Helper function to create field option elements.
*
* Field option elements are pieces that help create a field option.
* They are used to quickly build field options.
*
* @since 1.0.0
*
* @param string $option Field option to render.
* @param array $field Field data and settings.
* @param array $args Field preview arguments.
* @param bool $echo Print or return the value. Print by default.
*
* @return mixed echo or return string
*/
public function field_element( $option, $field, $args = array(), $echo = true ) {
$id = (int) $field['id'];
$class = ! empty( $args['class'] ) ? wpforms_sanitize_classes( (array) $args['class'], true ) : '';
$slug = ! empty( $args['slug'] ) ? sanitize_title( $args['slug'] ) : '';
$attrs = '';
$output = '';
if ( ! empty( $args['data'] ) ) {
foreach ( $args['data'] as $arg_key => $val ) {
if ( is_array( $val ) ) {
$val = wp_json_encode( $val );
}
$attrs .= ' data-' . $arg_key . '=\'' . $val . '\'';
}
}
if ( ! empty( $args['attrs'] ) ) {
foreach ( $args['attrs'] as $arg_key => $val ) {
if ( is_array( $val ) ) {
$val = wp_json_encode( $val );
}
$attrs .= $arg_key . '=\'' . $val . '\'';
}
}
switch ( $option ) {
// Row.
case 'row':
$output = sprintf(
'