// phpcs:disable Generic.Commenting.DocComment.MissingShort
/** @noinspection AutoloadingIssuesInspection */
/** @noinspection PhpIllegalPsrClassPathInspection */
// phpcs:enable Generic.Commenting.DocComment.MissingShort
use WPForms\Forms\Fields\Base\Frontend as FrontendBase;
use WPForms\Forms\Fields\Helpers\RequirementsAlerts;
use WPForms\Forms\Fields\Traits\MultiFieldMenu as MultiFieldMenuTrait;
use WPForms\Forms\Fields\Traits\ReadOnlyField as ReadOnlyFieldTrait;
use WPForms\Forms\IconChoices;
use WPForms\Integrations\AI\Helpers as AIHelpers;
abstract class WPForms_Field {
* Common default field settings.
private const COMMON_DEFAULT_SETTINGS = [
* Full name of the field type, e.g. "Paragraph Text".
* Type of the field, eg "textarea".
* Font Awesome Icon used for the editor button, e.g. "fa-list".
* Field keywords for search, e.g. "checkbox, file, icon, upload".
* Priority order the field button should show inside the "Add Fields" tab.
* Field group the field belongs to.
public $group = 'standard';
* Placeholder to hold default value(s) for some field types.
* Default field settings.
public $default_settings;
* Current form ID in the admin builder.
* Instance of the Frontend class.
* Primary class constructor.
* @param bool $init Pass false to allow shortcutting the whole initialization, if needed.
public function __construct( $init = true ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks
// phpcs:disable WordPress.Security.NonceVerification
if ( isset( $_GET['form_id'] ) ) {
$this->form_id = absint( $_GET['form_id'] );
} elseif ( isset( $_POST['id'] ) ) {
$this->form_id = absint( $_POST['id'] );
// phpcs:enable WordPress.Security.NonceVerification
// Init field default settings.
$this->field_default_settings();
// Initialize a field's Frontend class.
$this->frontend_obj = $this->get_object( 'Frontend' );
protected function common_hooks(): void { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks
// Solution to get an object of the field class.
"wpforms_fields_get_field_object_{$this->type}",
add_filter( 'wpforms_field_new_default', [ $this, 'field_new_default' ] );
add_filter( 'wpforms_field_data', [ $this, 'field_data' ], 10, 2 );
add_filter( 'wpforms_builder_fields_buttons', [ $this, 'field_button' ], 15 );
// Add field keywords to the template fields.
add_filter( 'wpforms_setup_template_fields', [ $this, 'enhance_template_fields_with_keywords' ] );
add_action( "wpforms_builder_fields_options_{$this->type}", [ $this, 'field_options' ] );
add_action( "wpforms_builder_fields_previews_{$this->type}", [ $this, 'field_preview' ] );
add_action( "wp_ajax_wpforms_new_field_{$this->type}", [ $this, 'field_new' ] );
// Display field input elements on the front-end.
add_action( "wpforms_display_field_{$this->type}", [ $this, 'field_display_proxy' ], 10, 3 );
// Display field on the back-end.
add_filter( "wpforms_pro_admin_entries_edit_is_field_displayable_{$this->type}", '__return_true', 9 );
// Validation on submitting.
add_action( "wpforms_process_validate_{$this->type}", [ $this, 'validate' ], 10, 3 );
add_action( "wpforms_process_format_{$this->type}", [ $this, 'format' ], 10, 3 );
add_filter( 'wpforms_field_properties', [ $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 the field name for ajax error.
add_filter( 'wpforms_process_ajax_error_field_name', [ $this, 'ajax_error_field_name' ], 10, 4 );
// Add HTML line breaks before all newlines in Entry Preview.
add_filter( "wpforms_pro_fields_entry_preview_get_field_value_{$this->type}_field_after", 'nl2br', 100 );
// Add allowed HTML tags for the field label.
add_filter( 'wpforms_builder_strings', [ $this, 'add_allowed_label_html_tags' ] );
// Exclude empty dynamic choices from Entry Preview.
add_filter( 'wpforms_pro_fields_entry_preview_print_entry_preview_exclude_field', [ $this, 'exclude_empty_dynamic_choices' ], 10, 3 );
// Add classes to the builder field preview.
add_filter( 'wpforms_field_preview_class', [ $this, 'preview_field_class' ], 10, 2 );
* All systems go. Used by subclasses. Required.
* @since 1.5.0 Converted to abstract method, as it's required for all fields.
abstract public function init();
* Prefill the field value with either fallback or dynamic data.
* This needs to be public (although internal) to be used in WordPress hooks.
* @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 the current field.
if ( $this->type !== $field['type'] ) {
// Set the form data, so we can reuse it later, even on the front-end.
$this->form_data = $form_data;
if ( ! empty( $this->form_data['settings']['dynamic_population'] ) ) {
$properties = $this->field_prefill_value_property_dynamic( $properties, $field );
// Fallback data rewrites the dynamic because user-submitted data is more important.
return $this->field_prefill_value_property_fallback( $properties, $field );
* As we are processing user submitted data - ignore all admin-defined defaults.
* Preprocess choice-related fields only.
* @param array $field Field data and settings.
* @param array $properties Properties we are modifying.
public function field_prefill_remove_choices_defaults( $field, &$properties ): void {
// Skip this step on the admin page.
if ( is_admin() && ! wpforms_is_admin_page( 'entries', 'edit' ) ) {
! empty( $field['dynamic_choices'] ) ||
! empty( $field['choices'] )
static function ( &$value, $key ) {
if ( $key === 'default' ) {
if ( $value === 'wpforms-selected' ) {
* Whether the current field can be populated dynamically.
* @param array $properties Field properties.
* @param array $field Current field specific data.
public function is_dynamic_population_allowed( $properties, $field ) {
// Allow the population on the front-end only.
// For dynamic population we require $_GET.
if ( empty( $_GET ) ) { // phpcs:ignore
* Filters whether the current field can be populated dynamically.
* @param bool $allowed Whether the current field can be populated dynamically.
* @param array $properties Field properties.
* @param array $field Field data.
return (bool) 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:
* As 'primary' is our default input key, "wpf4_12_primary" and "wpf4_12" are the same.
* @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 ) ) {
// 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 ) ) {
$form_id = absint( $matches[1] );
$field_id = absint( $matches[2] );
if ( ! empty( $matches[3] ) ) {
$input = sanitize_key( trim( $matches[3], '_' ) );
// Both form and field IDs should be the same as the current form / field.
(int) $this->form_data['id'] !== $form_id ||
(int) $field['id'] !== $field_id
// Go to the next GET param.
if ( ! empty( $raw_value ) ) {
$this->field_prefill_remove_choices_defaults( $field, $properties );
is_string( $raw_value ) &&
wpforms_get_multi_fields(),
$raw_value = explode( '|', rawurldecode( $raw_value ) );
* Some fields (like checkboxes) support multiple selection.
* We do not support nested values, so omit them.
* Example: ?wpf771_19_wpforms[fields][19][address1]=test
* $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
* We support also pipe-separated values like this:
if ( is_array( $raw_value ) ) {
foreach ( $raw_value as $single_value ) {
$properties = $this->get_field_populated_single_property_value( $single_value, $input, $properties, $field );
$properties = $this->get_field_populated_single_property_value( $raw_value, $input, $properties, $field );
* Public version of get_field_populated_single_property_value() to use by external classes.
* @param string $raw_value Value from a GET param, always a string.
* @param string $input Represent a subfield inside the field. Maybe 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 used to prefill via dynamic or fallback population.
* Based on field data and current properties.
* @param string $raw_value Value from a GET param, always a string.
* @param string $input Represent a subfield inside the field. Maybe 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 ) {