/home/vianto5/heredit.mx/wp-content/plugins/wpforms/src/Pro/Admin/Entries/Import
Edit: /home/vianto5/heredit.mx/wp-content/plugins/wpforms/src/Pro/Admin/Entries/Import/EntryImporter.php (44036B)
*/
private $default_preparers = [
'email' => FieldPreparer\EmailPreparer::class,
'url' => FieldPreparer\UrlPreparer::class,
'phone' => FieldPreparer\PhonePreparer::class,
'select' => FieldPreparer\ChoicesPreparer::class,
'radio' => FieldPreparer\ChoicesPreparer::class,
'checkbox' => FieldPreparer\CheckboxPreparer::class,
'date-time' => FieldPreparer\DateTimePreparer::class,
'address' => FieldPreparer\AddressPreparer::class,
'name' => FieldPreparer\NamePreparer::class,
'number' => FieldPreparer\NumberPreparer::class,
'number-slider' => FieldPreparer\NumberPreparer::class,
];
/**
* Instantiated preparer instances.
*
* @since 1.10.1
*
* @var array
*/
private $preparer_instances = [];
/**
* Constructor.
*
* @since 1.10.1
*
* @param int $form_id Form ID.
* @param string $source_identifier Source identifier.
* @param FailedEntriesCsv $failed_entries_csv Failed entries CSV writer.
*/
public function __construct( int $form_id, string $source_identifier, FailedEntriesCsv $failed_entries_csv ) {
$this->form_id = $form_id;
$this->source_identifier = $source_identifier;
$this->failed_entries_csv = $failed_entries_csv;
}
/**
* Return an instantiated source object for the given identifier.
*
* @since 1.10.1
*
* @param string $source_type Source type identifier: a file extension (e.g. 'csv') or plugin slug (e.g. 'cf7').
* @param array $source_args Constructor arguments for the source class.
*
* @return AbstractSource
*
* @throws InvalidArgumentException When no source class is registered for the given identifier.
*/
public static function get_source( string $source_type, array $source_args = [] ): AbstractSource {
$source = Files::get_by_extension( $source_type ) ?? Plugins::get_by_slug( $source_type );
if ( $source === null ) {
throw new InvalidArgumentException(
esc_html( sprintf( /* translators: %s - source identifier string. */ __( 'No import source registered for identifier "%s".', 'wpforms' ), $source_type ) )
);
}
if ( empty( $source_args ) ) {
return $source;
}
$class = get_class( $source );
return new $class( ...$source_args );
}
/**
* Get the list of supported destination field types for entry import.
*
* @since 1.10.1
*
* @return array
*/
public static function get_supported_destination_fields(): array {
/**
* Filter the list of supported destination field types for entry import.
*
* @since 1.10.1
*
* @param array $field_types Array of supported field type slugs.
*/
return (array) apply_filters(
'wpforms_pro_admin_entries_import_entry_importer_get_supported_destination_fields',
[
'address',
'checkbox',
'date-time',
'email',
'hidden',
'name',
'number',
'number-slider',
'password',
'phone',
'radio',
'rating',
'richtext',
'select',
'text',
'textarea',
'url',
]
);
}
/**
* Retrieve the destination fields for the form.
*
* @since 1.10.1
*
* @return array
*
* @throws RuntimeException If the form has no supported fields.
*/
public function get_destination_fields(): array {
$form_fields = wpforms_get_form_fields( $this->form_id, self::get_supported_destination_fields() );
$no_fields_error = __( 'Form has no supported fields.', 'wpforms' );
if ( empty( $form_fields ) ) {
throw new RuntimeException( esc_html( $no_fields_error ) );
}
$fields = $this->append_subfields( $form_fields );
$destination_fields = $this->append_choices( $fields, $form_fields );
if ( empty( $destination_fields ) ) {
throw new RuntimeException( esc_html( $no_fields_error ) );
}
return $destination_fields;
}
/**
* Build destination fields with all subfields expanded for Name, Address, and Date/Time.
*
* Always shows all available subfields regardless of field format.
*
* @since 1.10.1
*
* @param array $form_fields Array of form field settings.
*
* @return array
*/
private function append_subfields( array $form_fields ): array {
$fields = [];
foreach ( $form_fields as $field_id => $field ) {
if ( empty( $field['type'] ) || ! isset( $field['id'] ) ) {
continue;
}
$type = $field['type'];
// Name field: always show first, middle, last.
if ( $type === 'name' ) {
foreach ( [ 'first', 'middle', 'last' ] as $subfield ) {
$fields[] = [
'key' => "$field_id.$subfield",
'label' => WPFormsDbSource::get_field_label( $field, 'label', $subfield ),
];
}
continue;
}
// Date/Time field: always show date and time.
if ( $type === 'date-time' ) {
foreach ( [ 'date', 'time' ] as $subfield ) {
$fields[] = [
'key' => "$field_id.$subfield",
'label' => WPFormsDbSource::get_field_label( $field, 'label', $subfield ),
];
}
continue;
}
// Address field: show all subfields based on scheme.
if ( $type === 'address' ) {
WPFormsDbSource::append_address_subfields( $field, $fields );
continue;
}
/**
* Allow addons to expand a custom field type into multiple destination subfields.
*
* Return an array of destination subfield rows (each with `key` and `label`).
* Returning an empty array keeps the field as a single destination column.
*
* @since 1.10.1
*
* @param array $expanded Destination subfields. Default empty array.
* @param array $field Field settings.
*/
$expanded = (array) apply_filters(
'wpforms_pro_admin_entries_import_entry_importer_append_subfields_for_type',
[],
$field
);
if ( ! empty( $expanded ) ) {
$fields = array_merge( $fields, $expanded );
continue;
}
// Other fields: add as single field.
$fields[] = [
'key' => $field_id,
'label' => WPFormsDbSource::get_field_label( $field ),
];
}
return $fields;
}
/**
* Append choices from the provided fields to destination fields.
*
* For multiple-selection fields (checkbox, multi-select), expands each choice
* into a separate mappable field.
*
* @since 1.10.1
*
* @param array $fields Array of fields to process.
* @param array $form_fields Array of form field settings.
*
* @return array
*/
private function append_choices( array $fields, array $form_fields ): array {
$destination_fields = [];
foreach ( $fields as $field ) {
if ( ! isset( $field['key'], $field['label'] ) ) {
continue;
}
$field_id = $field['key'];
$field_settings = $form_fields[ $field_id ] ?? [];
if ( ! $this->is_multiple_field( $field_settings ) ) {
$destination_fields[] = $field;
continue;
}
$choices = $this->get_multiple_field_choices( $field_settings );
if ( empty( $choices ) ) {
$destination_fields[] = $field;
continue;
}
if ( ! isset( $field_settings['id'] ) || ! is_numeric( $field_settings['id'] ) ) {
$destination_fields[] = $field;
continue;
}
foreach ( $choices as $choice_id => $choice ) {
$destination_fields[] = [
'key' => "$field_id.$choice_id",
'label' => isset( $choice['label'] ) && ! wpforms_is_empty_string( $choice['label'] )
? $field['label'] . ': ' . $choice['label']
/* translators: %s - choice number. */
: sprintf( esc_html__( 'Choice %s', 'wpforms' ), $choice_id ),
];
}
}
return $destination_fields;
}
/**
* Import entries based on the provided mapping and update the result array.
*
* @since 1.10.1
*
* @param array $results Reference to an array that will store the import results.
* @param array $form_data Form data and settings.
* @param array $map The mapping configuration for the import process.
*
* @return int The number of successful entries imported.
*/
public function import_entries( array &$results, array $form_data, array $map ): int {
$imported = 0;
$meta_map = [];
$results['errors'] = $results['errors'] ?? [];
if ( isset( $map['entry_date'] ) ) {
$meta_map['entry_date'] = $map['entry_date'];
unset( $map['entry_date'] );
}
foreach ( $results['entries'] as $source_entry ) {
$entry_id = $this->process_source_entry( $source_entry, $form_data, $map, $meta_map, $results['errors'] );
if ( $entry_id ) {
++$imported;
}
}
return $imported;
}
/**
* Process a single source entry: validate, prepare fields, create the destination entry, and collect errors.
*
* @since 1.10.1
*
* @param array $source_entry Source entry data.
* @param array $form_data Destination form data.
* @param array $map Field mapping configuration.
* @param array $meta_map Meta fields mapping configuration.
* @param array $errors Errors array, passed by reference.
*
* @return int Created entry ID, or 0 when the entry was skipped.
*/
private function process_source_entry( array $source_entry, array $form_data, array $map, array $meta_map, array &$errors ): int {
$source_entry_id = $source_entry['id'] ?? 0;
if ( empty( $source_entry['fields'] ) || array_filter( $source_entry['fields'], 'strlen' ) === [] ) {
$errors[] = $this->make_source_error( $source_entry_id, esc_html__( 'Empty Entry. The record was skipped.', 'wpforms' ) );
return 0;
}
$entry_errors = [];
$fields = $this->prepare_fields( $source_entry['fields'], $map, $form_data, $entry_errors );
if ( empty( $fields ) ) {
$errors[] = $this->make_source_error( $source_entry_id, esc_html__( 'No fields were mapped. The record was skipped.', 'wpforms' ) );
$this->failed_entries_csv->record( $source_entry['fields'] );
return 0;
}
$entry_meta = $source_entry['meta'] ?? [];
// When a source field is mapped to Entry Date, use its value as the entry date.
if ( ! empty( $meta_map['entry_date'] ) && $meta_map['entry_date'] !== 'current' && isset( $source_entry['fields'][ $meta_map['entry_date'] ] ) ) {
$entry_meta['date'] = (string) $source_entry['fields'][ $meta_map['entry_date'] ];
}
$this->merge_fields_extra( $fields, $source_entry, $map );
$entry_id = $this->create_entry( $fields, $form_data, $entry_meta );
if ( ! $entry_id ) {
$errors[] = $this->make_source_error( $source_entry_id, esc_html__( 'Entry creation failed. The record was skipped.', 'wpforms' ) );
$this->failed_entries_csv->record( $source_entry['fields'] );
return 0;
}
/**
* Fires after a single entry is successfully imported.
*
* @since 1.10.1
*
* @param int $entry_id Newly created entry ID.
* @param int $source_entry_id Source entry ID (0 if not available).
* @param int $form_id Destination form ID.
* @param string $source_identifier Source identifier (e.g., 'wpforms', 'csv').
*/
do_action(
'wpforms_pro_admin_entries_import_entry_importer_entry_imported',
$entry_id,
$source_entry_id,
$this->form_id,
$this->source_identifier
);
foreach ( $entry_errors as $error ) {
$errors[] = wp_parse_args(
$error,
[
'type' => 'destination',
'entry_id' => $entry_id,
'value' => '',
'field' => '',
'message' => '',
'status' => 'skipped',
]
);
}
return $entry_id;
}
/**
* Merge per-field extras from the source entry into the formatted destination fields.
*
* Extras are extra keys (e.g. quiz_result) that sources attach to the source
* entry payload via `fields_extra`, keyed by source field ID. The map is used
* to translate each source ID into the matching destination field ID(s) so the
* extras land in the right `$fields` row regardless of whether the source and
* destination forms share field IDs.
*
* @since 1.10.1
*
* @param array $fields Formatted destination fields keyed by destination field ID, passed by reference.
* @param array $source_entry Source entry payload (id, fields, fields_extra, meta).
* @param array $map Map of destination field ID (string) to source field ID (string).
*
* @return void
*/
private function merge_fields_extra( array &$fields, array $source_entry, array $map ): void {
if ( empty( $source_entry['fields_extra'] ) ) {
return;
}
foreach ( $source_entry['fields_extra'] as $source_field_id => $extras ) {
$dest_field_keys = array_keys( $map, (string) $source_field_id, true );
foreach ( $dest_field_keys as $dest_field_key ) {
$dest_field_id = (int) explode( '.', (string) $dest_field_key )[0];
if ( ! isset( $fields[ $dest_field_id ] ) || ! is_array( $fields[ $dest_field_id ] ) ) {
continue;
}
$fields[ $dest_field_id ] = array_merge( $fields[ $dest_field_id ], (array) $extras );
}
}
}
/**
* Build a source-level skipped-entry error record.
*
* @since 1.10.1
*
* @param int $source_entry_id Source entry ID.
* @param string $message Human-readable reason the entry was skipped.
*
* @return array
*/
private function make_source_error( int $source_entry_id, string $message ): array {
return [
'type' => 'source',
'entry_id' => $source_entry_id,
'status' => 'skipped',
'message' => $message,
];
}
/**
* Prepare (format) form fields from a source entry using the field map.
*
* @since 1.10.1
*
* @param array $source_entry Source entry data keyed by the source field name.
* @param array $map Map of source field name to destination field ID string.
* @param array $form_data Destination form data.
* @param array $errors List of errors.
*
* @return array Formatted fields keyed by field ID, ready for entry creation.
*/
private function prepare_fields( array $source_entry, array $map, array $form_data, array &$errors ): array {
if ( empty( $form_data['fields'] ) ) {
return [];
}
$process = wpforms()->obj( 'process' );
$process->fields = [];
$entries_edit = wpforms()->obj( 'entries_edit' );
$all_empty = true;
foreach ( $form_data['fields'] as $field_settings ) {
$field_id = $field_settings['id'] ?? '';
$field_type = $field_settings['type'] ?? '';
if ( empty( $field_type ) || wpforms_is_empty_string( $field_id ) ) {
continue;
}
$field_id = (int) $field_id;
$raw_source_value = $this->prepare_field_value( $field_settings, $source_entry, $map );
if ( ! $this->is_source_value_empty( $raw_source_value ) ) {
$all_empty = false;
}
$this->format_source_field( $field_id, $raw_source_value, $field_settings, $form_data, $entries_edit, $errors );
}
if ( $all_empty ) {
return [];
}
return $process->fields;
}
/**
* Determine if a raw source value is considered empty.
*
* @since 1.10.1
*
* @param string|array $value Raw source value returned by prepare_field_value().
*
* @return bool
*/
private function is_source_value_empty( $value ): bool {
return is_array( $value ) ? empty( $value ) : wpforms_is_empty_string( $value );
}
/**
* Format a single source field and collect validation errors.
*
* @since 1.10.1
*
* @param int $field_id Destination field ID.
* @param string|array $raw_source_value Raw value returned by prepare_field_value().
* @param array $field_settings Destination field settings.
* @param array $form_data Destination form data.
* @param object $entries_edit EntriesEdit object used for formatting.
* @param array $errors Validation errors, passed by reference.
*/
private function format_source_field( int $field_id, $raw_source_value, array $field_settings, array $form_data, object $entries_edit, array &$errors ): void { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks
// Raw import mode: skip all field formatting and validation, write data as-is.
if ( $this->is_raw_import_mode() ) {
$value = is_array( $raw_source_value ) ? implode( "\n", $raw_source_value ) : $raw_source_value;
$value = sanitize_textarea_field( $value );
if ( wpforms_is_empty_string( $value ) ) {
return;
}
$process = wpforms()->obj( 'process' );
$process->fields[ $field_id ] = [
'name' => $field_settings['label'] ?? '',
'value' => $value,
'id' => $field_id,
'type' => $field_settings['type'] ?? '',
];
return;
}
$sanitized_source_value = $this->prepare_source_value( $raw_source_value, $field_settings, $form_data );
$field_object = $entries_edit->get_entries_edit_field_object( $field_settings['type'] );
// Allow off-list values (Select / Checkbox / Radio without Other) to be
// written as-is during import rather than filtered to empty by the
// choice allowlist sanitizer.
add_filter( 'wpforms_field_choices_allow_unknown_value', '__return_true' ); // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks
$field_object->format(
$field_id,
$sanitized_source_value,
$entries_edit->get_empty_entry_field_data( $field_settings ),
$form_data
);
remove_filter( 'wpforms_field_choices_allow_unknown_value', '__return_true' ); // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks
$process = wpforms()->obj( 'process' );
$formatted_field = ! empty( $process->fields[ $field_id ] )
? (array) $process->fields[ $field_id ]
: [
'id' => $field_id,
'name' => $field_settings['label'] ?? '',
];
$field_type = (string) ( $field_settings['type'] ?? '' );
$this->validate_errors( $raw_source_value, $formatted_field, $field_type, $errors, $field_settings );
}
/**
* Sanitize source value to adjust for our current field format.
*
* @since 1.10.1
*
* @param string|array $raw_source_value Field value.
* @param array $field_settings Field settings.
* @param array $form_data Form data and settings.
*
* @return string|array
*/
private function prepare_source_value( $raw_source_value, array $field_settings, array $form_data ) {
$field_type = $field_settings['type'] ?? '';
if ( wpforms_is_empty_string( $field_type ) ) {
return $raw_source_value;
}
return $this->get_preparer_instance( $field_type )->prepare( $raw_source_value, $field_settings, $form_data );
}
/**
* Get the field preparers registry.
*
* @since 1.10.1
*
* @return array
*/
private function get_preparers(): array {
/**
* Filter the field preparers registry for entry import.
*
* Allows registering custom field preparer classes for specific field types.
* Each preparer must implement FieldPreparer\FieldPreparerInterface.
*
* @since 1.10.1
*
* @param array $preparers Map of field type slug to preparer class name.
* @param int $form_id Destination form ID.
* @param string $source_identifier Source identifier (e.g., 'csv', 'gravity-forms').
*/
return (array) apply_filters(
'wpforms_pro_admin_entries_import_entry_importer_get_preparers',
$this->default_preparers,
$this->form_id,
$this->source_identifier
);
}
/**
* Get or create a preparer instance for the given field type.
*
* Always returns a preparer — falls back to DefaultPreparer when no
* specific preparer is registered for the field type.
*
* @since 1.10.1
*
* @param string $field_type Field type.
*
* @return FieldPreparer\FieldPreparerInterface
*/
private function get_preparer_instance( string $field_type ): FieldPreparer\FieldPreparerInterface {
if ( isset( $this->preparer_instances[ $field_type ] ) ) {
return $this->preparer_instances[ $field_type ];
}
$preparers = $this->get_preparers();
$preparer_class = $preparers[ $field_type ] ?? FieldPreparer\DefaultPreparer::class;
if ( ! class_exists( $preparer_class ) ) {
$preparer_class = FieldPreparer\DefaultPreparer::class;
}
$preparer = new $preparer_class();
if ( ! $preparer instanceof FieldPreparer\FieldPreparerInterface ) {
$preparer = new FieldPreparer\DefaultPreparer();
}
$this->preparer_instances[ $field_type ] = $preparer;
return $preparer;
}
/**
* Validate source value errors.
*
* @since 1.10.1
*
* @param string|array $raw_source_value Field value.
* @param array $field_format Field format.
* @param string $field_type Destination field type (e.g. 'email', 'address').
* @param array $errors Error list.
* @param array $field_settings Destination field settings (used for re-parse normalization checks).
*/
private function validate_errors( $raw_source_value, array $field_format, string $field_type, array &$errors, array $field_settings = [] ): void { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks
$field_id = (int) ( $field_settings['id'] ?? 0 );
/**
* Filter the validation errors appended for a specific field type.
*
* Allows addons to override the generic per-subfield comparison loop for
* field types where it can't produce meaningful results (e.g., Map's
* choice-index storage). Return an array of error rows (possibly empty)
* to claim ownership of the field's validation and bypass the generic
* loop. Return null (default) to let the generic loop run.
*
* @since 1.10.1
*
* @param array|null $appended Error rows to append, or null to defer.
* @param int $field_id Destination field ID.
* @param array $field_format Formatted field payload after format() ran.
* @param array $field_settings Destination field settings.
* @param mixed $raw_source_value Raw source value passed to the field.
*/
$appended = apply_filters(
'wpforms_pro_admin_entries_import_entry_importer_validate_errors_appended',
null,
$field_id,
$field_format,
$field_settings,
$raw_source_value
);
if ( is_array( $appended ) ) {
$errors = array_merge( $errors, $appended );
return;
}
if ( is_array( $raw_source_value ) && $this->is_multiple_field( $field_settings ) ) {
$raw_source_value = implode( "\n", array_filter( $raw_source_value ) );
}
$values = is_string( $raw_source_value )
? [ 'value' => $raw_source_value ]
: $raw_source_value;
foreach ( $values as $key => $raw_source_value_item ) {
$this->compare_values( (string) $raw_source_value_item, $field_format, (string) $key, $field_type, $errors, $field_settings );
}
}
/**
* Compare the original source value with the formatted and sanitized value stored in a database.
*
* @since 1.10.1
*
* @param string $raw_source_value Field value.
* @param array $field_format Field format.
* @param string $subfield Field format subkey.
* @param string $field_type Destination field type.
* @param array $errors Error list.
* @param array $field_settings Destination field settings (used for re-parse normalization checks).
*/
private function compare_values( string $raw_source_value, array $field_format, string $subfield, string $field_type, array &$errors, array $field_settings = [] ): void {
if ( wpforms_is_empty_string( $raw_source_value ) ) {
return;
}
$formatted_value = $field_format[ $subfield ] ?? '';
if ( $raw_source_value === (string) $formatted_value ) {
return;
}
$label = WPFormsDbSource::get_field_label( $field_format, 'name', $subfield );
$status = $formatted_value === '' ? 'skipped' : 'fixed';
$preparer = $this->get_preparer_instance( $field_type );
// Suppress the "fixed" flag when the raw value, fed through the preparer,
// naturally produces the same subfield value (e.g. extracting the date
// portion from "03/25/2026 10:30 AM" or normalizing ISO to m/d/Y).
// The data is preserved exactly as the preparer would normally render it,
// so there is nothing to flag.
if ( $status === 'fixed' && $field_settings !== [] && $this->is_normalized_subfield( $preparer, $raw_source_value, (string) $formatted_value, $subfield, $field_settings ) ) {
return;
}
$message_subfield = $subfield === 'value' ? '' : $subfield;
$message = $preparer->get_error_message( $status, $message_subfield );
/**
* Filter the per-field error message emitted during entry import.
*
* @since 1.10.1
*
* @param string $message The resolved error message.
* @param string $status Error status: 'skipped' or 'fixed'.
* @param string $field_type Destination field type slug.
* @param string $subfield Subfield key, or empty string if none.
* @param array $field_format Field format data used to derive the message.
*/
$message = (string) apply_filters(
'wpforms_pro_admin_entries_import_entry_importer_get_error_message',
$message,
$status,
$field_type,
$message_subfield,
$field_format
);
if ( $message === '' ) {
return;
}
$errors[] = [
'value' => $raw_source_value,
'field' => $label,
'status' => $status,
'message' => $message,
];
}
/**
* Decide whether the formatted subfield value is just a normalization of the raw input.
*
* @since 1.10.1
*
* @param FieldPreparer\FieldPreparerInterface $preparer Preparer instance.
* @param string $raw_source_value Raw value from the source.
* @param string $formatted_value Formatted value stored on the field.
* @param string $subfield Subfield key (e.g. 'date', 'time').
* @param array $field_settings Destination field settings.
*
* @return bool
*/
private function is_normalized_subfield( FieldPreparer\FieldPreparerInterface $preparer, string $raw_source_value, string $formatted_value, string $subfield, array $field_settings ): bool {
$reparsed = $preparer->prepare( $raw_source_value, $field_settings, [] );
if ( ! is_array( $reparsed ) ) {
return false;
}
// Radio "Other" path: the preparer returns [ 'other' => $x ], which
// Radio::format() then stores as $field['value']. Treat as normalization
// when the formatted 'value' equals the preparer's 'other'.
if ( $subfield === 'value' && isset( $reparsed['other'] ) ) {
return $formatted_value === (string) $reparsed['other'];
}
$reparsed_value = (string) ( $reparsed[ $subfield ] ?? '' );
return $formatted_value === $reparsed_value;
}
/**
* Prepare the value for a given field based on its settings and mapping configuration.
*
* @since 1.10.1
*
* @param array $field_settings Configuration settings for the field.
* @param array $source_entry The source entry containing the data to map.
* @param array $map Mapping configuration for processing the field.
*
* @return string|array The prepared value or subfield values for the field.
*/
private function prepare_field_value( array $field_settings, array $source_entry, array $map ) {
if ( $this->is_multiple_field( $field_settings ) ) {
return $this->prepare_multiple_field_value( $field_settings, $source_entry, $map );
}
// Name, Date/Time, and Address fields always have subfields in the destination column dropdown
// (see append_subfields), so we must use subfield handling regardless of the source format.
$type = $field_settings['type'] ?? '';
$always_has_subfields = in_array( $type, [ 'name', 'date-time', 'address' ], true );
if ( ! $always_has_subfields && ! WPFormsDbSource::field_has_subfields( $field_settings ) ) {
return $this->get_field_submit_value( $source_entry, $map, $field_settings );
}
return $this->prepare_subfields_value( $field_settings, $source_entry, $map, $type, $always_has_subfields );
}
/**
* Prepare the value for a multiple-choice field (checkboxes/multi-select style).
*
* @since 1.10.1
*
* @param array $field_settings Configuration settings for the field.
* @param array $source_entry The source entry containing the data to map.
* @param array $map Mapping configuration for processing the field.
*
* @return string|array
*/
private function prepare_multiple_field_value( array $field_settings, array $source_entry, array $map ) {
$field_id = (int) ( $field_settings['id'] ?? '' );
$field_submit = [];
$source_columns = [];
$choices = $this->get_multiple_field_choices( $field_settings );
foreach ( $choices as $choice_id => $choice ) {
$field_settings['id'] = "$field_id.$choice_id";
$current_field_id = (string) $field_settings['id'];
$value = $this->get_field_submit_value( $source_entry, $map, $field_settings );
if ( wpforms_is_empty_string( $value ) ) {
continue;
}
$field_submit[ $choice_id ] = $value;
if ( isset( $map[ $current_field_id ] ) ) {
$source_columns[ $choice_id ] = $map[ $current_field_id ];
}
}
if ( empty( $field_submit ) ) {
return '';
}
// Compare source columns rather than values so different columns carrying identical
// data (e.g. a multi-row Likert where every row received the same answer) aren't
// false-collapsed into a single string the preparer can't re-split. Collapse on a
// single shared source column regardless of how many destination choices the user
// mapped, so a single mapping still routes through the preparer's delimiter split.
if ( count( array_unique( $source_columns ) ) === 1 ) {
return reset( $field_submit );
}
return $field_submit;
}
/**
* Prepare the value for a field with subfields (Name, Date/Time, Address, etc.).
*
* @since 1.10.1
*
* @param array $field_settings Configuration settings for the field.
* @param array $source_entry The source entry containing the data to map.
* @param array $map Mapping configuration for processing the field.
* @param string $type Field type.
* @param bool $always_has_subfields Whether the field type always renders subfields in the destination dropdown.
*
* @return array
*/
private function prepare_subfields_value( array $field_settings, array $source_entry, array $map, string $type, bool $always_has_subfields ): array {
$subfields = [];
WPFormsDbSource::append_field_subfields( $field_settings, $subfields );
// If no subfields were returned but this field type always shows subfields in destination dropdown,
// add the expected subfields manually. This handles cases like Name fields with "simple" format
// where the source doesn't have subfields, but the destination dropdown shows them.
if ( empty( $subfields ) && $always_has_subfields ) {
$subfields = $this->get_default_subfields( $field_settings, $type );
}
[ $field_submit, $source_columns ] = $this->collect_subfield_values( $subfields, $field_settings, $source_entry, $map );
if ( empty( $field_submit ) ) {
return $field_submit;
}
// If all subfields map to the same source column (single column mapped to the whole field),
// keep only the first subfield to avoid duplication.
// We check source columns, not values, to avoid false positives when different columns
// happen to contain identical data.
if ( count( $field_submit ) > 1 && count( array_unique( $source_columns ) ) === 1 ) {
reset( $field_submit );
$first_key = key( $field_submit );
return [ $first_key => current( $field_submit ) ];
}
return $field_submit;
}
/**
* Build the default subfield list for field types whose destination dropdown always shows subfields.
*
* @since 1.10.1
*
* @param array $field_settings Configuration settings for the field.
* @param string $type Field type.
*
* @return array
*/
private function get_default_subfields( array $field_settings, string $type ): array {
$field_id = $field_settings['id'];
$defaults = [
'name' => [ 'first', 'middle', 'last' ],
'date-time' => [ 'date', 'time' ],
'address' => [ 'address1', 'address2', 'city', 'state', 'postal', 'country' ],
];
$subfields = [];
foreach ( $defaults[ $type ] ?? [] as $subfield ) {
$subfields[] = [
'key' => "$field_id.$subfield",
'label' => WPFormsDbSource::get_field_label( $field_settings, 'label', $subfield ),
];
}
return $subfields;
}
/**
* Collect submit values and source-column mappings for each subfield.
*
* @since 1.10.1
*
* @param array $subfields Subfield descriptors with 'key' entries.
* @param array $field_settings Configuration settings for the field.
* @param array $source_entry The source entry containing the data to map.
* @param array $map Mapping configuration for processing the field.
*
* @return array{0: array, 1: array} Tuple of [ field_submit, source_columns ].
*/
private function collect_subfield_values( array $subfields, array $field_settings, array $source_entry, array $map ): array {
$field_submit = [];
$source_columns = [];
foreach ( $subfields as $subfield ) {
$field_settings['id'] = $subfield['key'];
$field_id_parts = explode( '.', $field_settings['id'] );
$field_subfield = isset( $field_id_parts[1] ) ? (string) $field_id_parts[1] : 'value';
$current_field_id = (string) $field_settings['id'];
$value = $this->get_field_submit_value( $source_entry, $map, $field_settings );
if ( wpforms_is_empty_string( $value ) ) {
continue;
}
$field_submit[ $field_subfield ] = $value;
// Track which source column this subfield maps to.
if ( isset( $map[ $current_field_id ] ) ) {
$source_columns[ $field_subfield ] = $map[ $current_field_id ];
}
}
return [ $field_submit, $source_columns ];
}
/**
* Build the field submit value from the source entry using the field map.
*
* @since 1.10.1
*
* @param array $source_entry Source entry data keyed by the source field name.
* @param array $map Map of source field name to destination field ID string.
* @param array $field_settings Field settings.
*
* @return string
*/
private function get_field_submit_value( array $source_entry, array $map, array $field_settings ): string {
$field_id = (string) $field_settings['id'];
if ( ! isset( $map[ $field_id ] ) ) {
return '';
}
$source_field_id = $map[ $field_id ];
return isset( $source_entry[ $source_field_id ] ) ? (string) $source_entry[ $source_field_id ] : '';
}
/**
* Normalize a raw date string to UTC Y-m-d H:i:s.
*
* Returns the current UTC time when $raw_date is empty or unparseable.
*
* @since 1.10.1
*
* @param string $raw_date Raw date string from the source.
*
* @return string
*/
private function normalize_entry_date( string $raw_date ): string {
$fallback = current_time( self::ENTRY_DATE_FORMAT, true );
if ( $raw_date === '0' || wpforms_is_empty_string( $raw_date ) ) {
return $fallback;
}
// Unix timestamps must be handled explicitly; DateTime constructor does not accept them.
if ( ctype_digit( $raw_date ) ) {
$timestamp = (int) $raw_date;
if ( $timestamp < self::MIN_ENTRY_TIMESTAMP || $timestamp > time() ) {
return $fallback;
}
return gmdate( self::ENTRY_DATE_FORMAT, $timestamp );
}
try {
$date = new DateTime( $raw_date, new DateTimeZone( 'UTC' ) );
$timestamp = $date->getTimestamp();
if ( $timestamp < self::MIN_ENTRY_TIMESTAMP || $timestamp > time() ) {
return $fallback;
}
return $date->format( self::ENTRY_DATE_FORMAT );
} catch ( Exception $e ) {
return $fallback;
}
}
/**
* Persist entry-meta rows to the wpforms_entry_meta table.
*
* @since 1.10.1
*
* @param int $entry_id Newly created entry ID.
* @param array $entry_meta Map of meta type => data to persist.
*/
private function save_entry_meta( int $entry_id, array $entry_meta ): void {
$handler = wpforms()->obj( 'entry_meta' );
if ( ! $handler ) {
return;
}
// Notes and logs carry authorship; attribute imported rows to the importing user
// so downstream UI (entry single, print preview) can resolve a valid WP user.
$authored_types = [ 'note', 'log' ];
$current_user = get_current_user_id();
foreach ( $entry_meta as $type => $data ) {
if ( wpforms_is_empty_string( $type ) ) {
continue;
}
$type = sanitize_key( $type );
$items = is_array( $data ) ? $data : [ $data ];
foreach ( $items as $item ) {
if ( wpforms_is_empty_string( $item ) ) {
continue;
}
$row = [
'entry_id' => $entry_id,
'form_id' => $this->form_id,
'type' => $type,
'data' => $item,
];
if ( in_array( $type, $authored_types, true ) ) {
$row['user_id'] = $current_user;
}
$handler->add( $row, 'entry_meta' );
}
}
}
/**
* Persist a formatted entry to the database.
*
* @since 1.10.1
*
* @param array $fields Formatted fields from prepare_fields().
* @param array $form_data Destination form data.
* @param array $entry_meta Flat meta array. Known entry-table column keys
* (date, ip_address, user_agent, user_uuid, user_id, viewed, starred)
* are written to wpforms_entries; every other key is stored as a
* row in wpforms_entry_meta.
*
* @return int Created entry ID, or 0 on failure.
*/
private function create_entry( array $fields, array $form_data, array $entry_meta = [] ): int {
$date = $this->normalize_entry_date( $entry_meta['date'] ?? '' );
$entry_args = [
'form_id' => $this->form_id,
'type' => 'imported',
'fields' => wp_json_encode( $fields ),
'date' => $date,
'date_modified' => $date,
'user_id' => isset( $entry_meta['user_id'] ) ? (int) $entry_meta['user_id'] : 0,
'viewed' => isset( $entry_meta['viewed'] ) ? (int) $entry_meta['viewed'] : 0,
'starred' => isset( $entry_meta['starred'] ) ? (int) $entry_meta['starred'] : 0,
];
$entry_args = array_merge( $entry_args, $this->get_gdpr_entry_args( $entry_meta, $form_data ) );
$entry_id = (int) wpforms()->obj( 'entry' )->add( $entry_args, 'entry' );
if ( ! $entry_id ) {
return 0;
}
wpforms()->obj( 'entry_fields' )->save( $fields, $form_data, $entry_id );
$column_keys = [ 'date', 'ip_address', 'user_agent', 'user_uuid', 'user_id', 'viewed', 'starred' ];
$meta_rows = array_diff_key( $entry_meta, array_flip( $column_keys ) );
if ( ! empty( $meta_rows ) ) {
$this->save_entry_meta( $entry_id, $meta_rows );
}
return $entry_id;
}
/**
* Build the GDPR-gated entry args for IP address, user agent, and user UUID.
*
* @since 1.10.1
*
* @param array $entry_meta Flat meta array from the import source.
* @param array $form_data Destination form data.
*
* @return array Subset of entry args safe to merge into the entry row.
*/
private function get_gdpr_entry_args( array &$entry_meta, array $form_data ): array {
$args = [];
if ( ! empty( $entry_meta['user_uuid'] ) && wpforms_is_collecting_cookies_allowed() ) {
$args['user_uuid'] = sanitize_text_field( $entry_meta['user_uuid'] );
}
if ( ! wpforms_is_collecting_ip_allowed( $form_data ) ) {
unset( $entry_meta['location'] );
return $args;
}
if ( ! empty( $entry_meta['ip_address'] ) ) {
$args['ip_address'] = sanitize_text_field( $entry_meta['ip_address'] );
}
if ( ! empty( $entry_meta['user_agent'] ) ) {
$args['user_agent'] = sanitize_text_field( $entry_meta['user_agent'] );
}
return $args;
}
/**
* Get the iterable choices for a multiple-selection field.
*
* Defaults to the field's `choices` array. The result drives both the
* per-choice destination dropdown labels and the per-choice value loop
* during field preparation.
*
* @since 1.10.1
*
* @param array $field Field settings.
*
* @return array Map of choice key to choice data (must include a `label` key).
*/
private function get_multiple_field_choices( array $field ): array {
$choices = $field['choices'] ?? [];
/**
* Filter the iterable choices for a multiple-selection field.
*
* Allows addons to provide non-`choices`-based item lists (e.g., Likert
* Scale rows) that should drive destination expansion and per-item value
* mapping.
*
* @since 1.10.1
*
* @param array $choices Map of item key to item data. Each item must include a `label` key.
* @param array $field Field settings.
*/
return (array) apply_filters(
'wpforms_pro_admin_entries_import_entry_importer_get_multiple_field_choices',
$choices,
$field
);
}
/**
* Determine if a given field is a multiple selection field.
*
* @since 1.10.1
*
* @param array $field The field to check, containing type and configuration details.
*
* @return bool
*/
private function is_multiple_field( array $field ): bool {
if ( empty( $field['type'] ) ) {
return false;
}
$is_multiple = false;
if ( $field['type'] === 'checkbox' ) {
$is_multiple = true;
}
if ( $field['type'] === 'select' && ! empty( $field['multiple'] ) ) {
$is_multiple = true;
}
/**
* Filter whether a given field is treated as a multiple-selection field for entry import.
*
* Allows addons (e.g., Surveys & Polls) to register field types that should be
* expanded into per-choice destination columns in the import mapping UI.
*
* @since 1.10.1
*
* @param bool $is_multiple Whether the field is multiple-selection.
* @param array $field Field settings.
*/
return (bool) apply_filters(
'wpforms_pro_admin_entries_import_entry_importer_is_multiple_field',
$is_multiple,
$field
);
}
/**
* Check if raw import mode is enabled.
*
* When enabled, field values are written as-is without formatting or validation.
*
* @since 1.10.1
*
* @return bool
*/
private function is_raw_import_mode(): bool {
/**
* Filter to enable raw import mode.
*
* When enabled, field values are imported without any formatting or validation,
* preserving the exact source data.
*
* @since 1.10.1
*
* @param bool $is_raw_mode Whether raw import mode is enabled. Default false.
* @param int $form_id Destination form ID.
* @param string $source_identifier Source identifier (e.g., 'csv', 'gravity-forms').
*/
return (bool) apply_filters(
'wpforms_pro_admin_entries_import_entry_importer_is_raw_import_mode',
false,
$this->form_id,
$this->source_identifier
);
}
}