View source
<?php
namespace Drupal\webform_submission_export_import;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Serialization\Yaml;
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\webform\Plugin\WebformElement\WebformCompositeBase;
use Drupal\webform\Plugin\WebformElement\WebformLikert;
use Drupal\webform\Plugin\WebformElement\WebformManagedFileBase;
use Drupal\webform\Plugin\WebformElementEntityReferenceInterface;
use Drupal\webform\Plugin\WebformElementManagerInterface;
use Drupal\webform\EntityStorage\WebformEntityStorageTrait;
use Drupal\webform\WebformInterface;
use Drupal\webform\WebformSubmissionForm;
use Drupal\webform\WebformSubmissionInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Yaml\Dumper;
class WebformSubmissionExportImportImporter implements WebformSubmissionExportImportImporterInterface {
use StringTranslationTrait;
use WebformEntityStorageTrait;
protected $configFactory;
protected $loggerFactory;
protected $entityTypeManager;
protected $elementManager;
protected $webform;
protected $sourceEntity;
protected $importUri;
protected $importTotal;
protected $importOptions;
protected $elements;
protected $fieldDefinitions;
protected $fileSystem;
public function __construct(ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory, EntityTypeManagerInterface $entity_type_manager, WebformElementManagerInterface $element_manager, FileSystemInterface $file_system) {
$this->configFactory = $config_factory;
$this->loggerFactory = $logger_factory;
$this->entityTypeManager = $entity_type_manager;
$this->elementManager = $element_manager;
$this->fileSystem = $file_system;
}
public function setWebform(WebformInterface $webform = NULL) {
$this->webform = $webform;
$this->elementTypes = NULL;
}
public function getWebform() {
return $this->webform;
}
public function setSourceEntity(EntityInterface $entity = NULL) {
$this->sourceEntity = $entity;
}
public function getSourceEntity() {
return $this->sourceEntity;
}
public function getImportUri() {
return $this->importUri;
}
public function setImportUri($uri) {
$this->importUri = $uri;
$this->importTotal = NULL;
}
public function deleteImportUri() {
$files = $this
->getEntityStorage('file')
->loadByProperties([
'uri' => $this
->getImportUri(),
]);
if ($files) {
$file = reset($files);
$file
->delete();
return TRUE;
}
return FALSE;
}
public function getImportOptions() {
return $this->importOptions;
}
public function setImportOptions(array $options) {
$this->importOptions = $options + $this
->getDefaultImportOptions();
}
public function getImportOption($name) {
return $this->importOptions[$name] ?? NULL;
}
public function getDefaultImportOptions() {
return [
'skip_validation' => FALSE,
'treat_warnings_as_errors' => FALSE,
'mapping' => [],
];
}
public function getFieldDefinitions() {
if (isset($this->fieldDefinitions)) {
return $this->fieldDefinitions;
}
$this->fieldDefinitions = $this
->getSubmissionStorage()
->getFieldDefinitions();
return $this->fieldDefinitions;
}
public function getElements() {
if (isset($this->elements)) {
return $this->elements;
}
$this->elements = $this
->getWebform()
->getElementsInitializedFlattenedAndHasValue();
return $this->elements;
}
public function exportHeader() {
return array_keys($this
->getDestinationColumns());
}
public function exportSubmission(WebformSubmissionInterface $webform_submission, array $export_options = []) {
$submission_data = $webform_submission
->toArray(TRUE);
$record = [];
$field_definitions = $this
->getFieldDefinitions();
foreach ($field_definitions as $field_name => $field_definition) {
switch ($field_name) {
case 'uid':
$value = $this
->getEntityExportId($webform_submission
->getOwner(), $export_options);
break;
case 'entity_id':
$value = $this
->getEntityExportId($webform_submission
->getSourceEntity(), $export_options);
break;
default:
$value = isset($submission_data[$field_name]) ? $submission_data[$field_name] : '';
break;
}
$record[] = $this
->exportValue($value);
}
$elements = $this
->getElements();
foreach ($elements as $element) {
$element_plugin = $this->elementManager
->getElementInstance($element);
$has_multiple_values = $element_plugin
->hasMultipleValues($element);
if ($element_plugin instanceof WebformManagedFileBase) {
$files = $element_plugin
->getTargetEntities($element, $webform_submission) ?: [];
$values = [];
foreach ($files as $file) {
$values[] = file_create_url($file
->getFileUri());
}
$value = implode(',', $values);
$record[] = $this
->exportValue($value);
}
elseif ($element_plugin instanceof WebformElementEntityReferenceInterface) {
$entities = $element_plugin
->getTargetEntities($element, $webform_submission);
$values = [];
foreach ($entities as $entity) {
$values[] = $this
->getEntityExportId($entity, $export_options);
}
$value = implode(',', $values);
$record[] = $this
->exportValue($value);
}
elseif ($element_plugin instanceof WebformLikert) {
$value = $element_plugin
->getValue($element, $webform_submission);
$question_keys = array_keys($element['#questions']);
foreach ($question_keys as $question_key) {
$question_value = isset($value[$question_key]) ? $value[$question_key] : '';
$record[] = $this
->exportValue($question_value);
}
}
elseif ($element_plugin instanceof WebformCompositeBase && !$has_multiple_values) {
$value = $element_plugin
->getValue($element, $webform_submission);
$composite_element_keys = array_keys($element_plugin
->getCompositeElements());
foreach ($composite_element_keys as $composite_element_key) {
$composite_value = isset($value[$composite_element_key]) ? $value[$composite_element_key] : '';
$record[] = $this
->exportValue($composite_value);
}
}
elseif ($element_plugin
->isComposite()) {
$value = $element_plugin
->getValue($element, $webform_submission);
$dumper = new Dumper(2);
$record[] = $dumper
->dump($value);
}
elseif ($has_multiple_values) {
$values = $element_plugin
->getValue($element, $webform_submission);
$values = $values !== NULL ? (array) $values : [];
foreach ($values as $index => $value) {
$values[$index] = str_replace(',', '%2C', $value);
}
$value = implode(',', $values);
$record[] = $this
->exportValue($value);
}
else {
$value = $element_plugin
->getValue($element, $webform_submission);
$value = $value !== NULL ? $value : '';
$record[] = $this
->exportValue($value);
}
}
return $record;
}
public function import($offset = 0, $limit = NULL) {
if ($limit === NULL) {
$limit = $this
->getBatchLimit();
}
$import_options = $this
->getImportOptions();
$handle = fopen($this
->getImportUri(), 'r');
$column_names = fgetcsv($handle);
foreach ($column_names as $index => $name) {
$column_names[$index] = $name;
}
$index = 0;
while ($index < $offset && !feof($handle)) {
fgets($handle);
$index++;
}
$stats = [
'created' => 0,
'updated' => 0,
'skipped' => 0,
'total' => 0,
'warnings' => [],
'errors' => [],
];
while ($stats['total'] < $limit && !feof($handle)) {
$values = fgetcsv($handle);
if (empty($values) || $values === [
'',
]) {
continue;
}
$index++;
$stats['total']++;
$stats['warnings'][$index] = [];
$stats['errors'][$index] = [];
$row_warnings =& $stats['warnings'][$index];
$row_errors =& $stats['errors'][$index];
if (count($column_names) !== count($values)) {
$t_args = [
'@expected' => count($column_names),
'@found' => count($values),
];
$error = $this
->t('@expected values expected and only @found found.', $t_args);
if (!empty($import_options['treat_warnings_as_errors'])) {
$row_errors[] = $error;
}
else {
$row_warnings[] = $error;
}
continue;
}
$record = array_combine($column_names, $values);
foreach ($record as $key => $value) {
$record[$key] = trim($value);
}
$original_record = $record;
$record = $this
->importMapRecord($record);
if (empty($record['token'])) {
$record['token'] = Crypt::hashBase64(Settings::getHashSalt() . serialize($original_record));
}
$webform_submission = $this
->importLoadSubmission($record);
if ($errors = $this
->importPrepareRecord($record, $webform_submission)) {
if (!empty($import_options['treat_warnings_as_errors'])) {
$row_errors = array_merge($row_warnings, array_values($errors));
}
else {
$row_warnings = array_merge($row_warnings, array_values($errors));
}
}
if (empty($import_options['skip_validation'])) {
if ($errors = $this
->importValidateRecord($record)) {
$row_errors = array_merge($row_errors, array_values($errors));
}
}
if ($row_errors) {
$stats['skipped']++;
continue;
}
$this
->importSaveSubmission($record, $webform_submission);
$stats[$webform_submission ? 'updated' : 'created']++;
}
fclose($handle);
return $stats;
}
protected function importMapRecord(array $record) {
$mapping = $this
->getImportOption('mapping');
if (empty($mapping)) {
return $record;
}
$mapped_record = [];
foreach ($mapping as $source_name => $destination_name) {
if (isset($record[$source_name])) {
$mapped_record[$destination_name] = $record[$source_name];
}
}
return $mapped_record;
}
protected function importLoadSubmission(array &$record) {
$unique_keys = [
'uuid',
'token',
];
foreach ($unique_keys as $unique_key) {
if (!empty($record[$unique_key])) {
if ($webform_submissions = $this
->getSubmissionStorage()
->loadByProperties([
$unique_key => $record[$unique_key],
])) {
return reset($webform_submissions);
}
}
}
return NULL;
}
protected function importPrepareRecord(array &$record, WebformSubmissionInterface $webform_submission = NULL) {
$errors = [];
if (isset($record['uid'])) {
$record['uid'] = $this
->getEntityImportId('user', $record['uid']);
if (empty($record['uid'])) {
$record['uid'] = 0;
}
}
if (empty($record['uuid'])) {
unset($record['uuid']);
}
$webform = $this
->getWebform();
$source_entity = $this
->getSourceEntity();
$record['webform_id'] = $webform
->id();
if ($source_entity) {
$record['entity_type'] = $source_entity
->getEntityTypeId();
$record['entity_id'] = $source_entity
->id();
}
elseif (!empty($record['entity_type']) && isset($record['entity_id'])) {
$record['entity_id'] = $this
->getEntityImportId($record['entity_type'], $record['entity_id']);
if ($record['entity_id'] === NULL) {
$t_args = [
'@entity_type' => $record['entity_type'],
'@entity_id' => $record['entity_id'],
];
$errors[] = $this
->t('Unable to locate source entity (@entity_type:@entity_id)', $t_args);
$record['entity_type'] = NULL;
}
}
$elements = $this
->getElements();
foreach ($record as $name => $value) {
if (isset($elements[$name])) {
$element = $elements[$name];
$record[$name] = $this
->importElement($element, $value, $webform_submission, $errors);
continue;
}
if (strpos($name, '__') === FALSE) {
continue;
}
list($element_key, $composite_key) = explode('__', $name);
if (!isset($elements[$element_key])) {
continue;
}
$element = $elements[$element_key];
$element_plugin = $this->elementManager
->getElementInstance($element);
if ($element_plugin
->hasMultipleValues($element)) {
continue;
}
if ($element_plugin instanceof WebformLikert) {
if (!isset($element['#questions']) || !isset($element['#questions'][$composite_key])) {
continue;
}
$record[$element_key][$composite_key] = $value;
}
elseif ($element_plugin instanceof WebformCompositeBase) {
$composite_elements = $element_plugin
->getCompositeElements();
if (!isset($composite_elements[$composite_key])) {
continue;
}
$composite_element = $composite_elements[$composite_key];
$record[$element_key][$composite_key] = $this
->importElement($composite_element, $value, $webform_submission, $errors);
}
}
return $errors;
}
protected function importElement(array $element, $value, WebformSubmissionInterface $webform_submission = NULL, array &$errors) {
$element_plugin = $this->elementManager
->getElementInstance($element);
if ($value === '') {
return $element_plugin
->hasMultipleValues($element) ? [] : NULL;
}
elseif ($element_plugin instanceof WebformManagedFileBase) {
return $this
->importManageFileElement($element, $value, $webform_submission, $errors);
}
elseif ($element_plugin instanceof WebformElementEntityReferenceInterface) {
return $this
->importEntityReferenceElement($element, $value, $webform_submission, $errors);
}
elseif ($element_plugin
->isComposite()) {
return $this
->importCompositeElement($element, $value, $webform_submission, $errors);
}
elseif ($element_plugin
->hasMultipleValues($element)) {
return $this
->importMultipleElement($element, $value, $webform_submission, $errors);
}
else {
return $value;
}
}
protected function importManageFileElement(array $element, $value, WebformSubmissionInterface $webform_submission = NULL, array &$errors) {
$webform = $this
->getWebform();
$element_plugin = $this->elementManager
->getElementInstance($element);
$element_plugin
->prepare($element, $this
->getSubmissionStorage()
->create([
'webform_id' => $webform
->id(),
]));
$file_destination = isset($element['#upload_location']) ? $element['#upload_location'] : NULL;
if (isset($file_destination) && !$this->fileSystem
->prepareDirectory($file_destination, FileSystemInterface::CREATE_DIRECTORY)) {
$this->loggerFactory
->get('file')
->notice('The upload directory %directory for the file element %name could not be created or is not accessible. A newly uploaded file could not be saved in this directory as a consequence, and the upload was canceled.', [
'%directory' => $file_destination,
'%name' => $element['#webform_key'],
]);
return $element_plugin
->hasMultipleValues($element) ? [] : NULL;
}
$file_upload_validators = $element['#upload_validators'];
$existing_file_ids = [];
$existing_file_uris = [];
$existing_files = $webform_submission ? $element_plugin
->getTargetEntities($element, $webform_submission) ?: [] : [];
foreach ($existing_files as $existing_file) {
$existing_file_uri = file_create_url($existing_file
->getFileUri());
$existing_file_uris[$existing_file_uri] = $existing_file
->id();
$existing_file_hash = sha1_file($existing_file
->getFileUri());
$existing_file_ids[$existing_file_hash] = $existing_file
->id();
}
$new_file_ids = [];
$new_file_uris = explode(',', $value);
foreach ($new_file_uris as $new_file_uri) {
if (isset($existing_file_uris[$new_file_uri])) {
$new_file_ids[$new_file_uri] = $existing_file_uris[$new_file_uri];
continue;
}
$t_args = [
'@element_key' => $element['#webform_key'],
'@url' => $new_file_uri,
];
if (!preg_match('#^https?://#', $new_file_uri)) {
$errors[] = $this
->t('[@element_key] Invalid file URL (@url). URLS must begin with http:// or https://.', $t_args);
continue;
}
$file_headers = @get_headers($new_file_uri);
if (!$file_headers || $file_headers[0] === 'HTTP/1.1 404 Not Found') {
$errors[] = $this
->t('[@element_key] URL (@url) returns 404 file not found.', $t_args);
continue;
}
$new_file_hash = @sha1_file($new_file_uri);
if (!$new_file_hash) {
$errors[] = $this
->t('[@element_key] Unable to read file from URL (@url).', $t_args);
continue;
}
if (isset($existing_file_ids[$new_file_hash])) {
$new_file_ids[$new_file_hash] = $existing_file_ids[$new_file_hash];
continue;
}
$temp_file_contents = @file_get_contents($new_file_uri);
if (!$temp_file_contents) {
$errors[] = $this
->t('[@element_key] Unable to read file from URL (@url).', $t_args);
continue;
}
$handle = tmpfile();
fwrite($handle, $temp_file_contents);
$temp_file_meta_data = stream_get_meta_data($handle);
$temp_file_path = $temp_file_meta_data['uri'];
$temp_file_size = filesize($temp_file_path);
$temp_file_info = new UploadedFile($temp_file_path, basename($new_file_uri), NULL, $temp_file_size);
$webform_element_key = $element_plugin
->getLabel($element);
$new_file = _webform_submission_export_import_file_save_upload_single($temp_file_info, $webform_element_key, $file_upload_validators, $file_destination);
if ($new_file) {
$new_file_ids[$new_file_hash] = $new_file
->id();
}
}
$values = array_values($new_file_ids);
if (empty($values)) {
return $element_plugin
->hasMultipleValues($element) ? [] : NULL;
}
else {
return $element_plugin
->hasMultipleValues($element) ? $values : reset($values);
}
}
protected function importEntityReferenceElement(array $element, $value, WebformSubmissionInterface $webform_submission = NULL, array &$errors) {
$element_plugin = $this->elementManager
->getElementInstance($element);
$entity_type_id = $element_plugin
->getTargetType($element);
$values = explode(',', $value);
foreach ($values as $index => $value) {
$values[$index] = $this
->getEntityImportId($entity_type_id, $value);
if ($values[$index] === NULL) {
$t_args = [
'@element_key' => $element['#webform_key'],
'@entity_id' => $value,
];
$errors[] = $this
->t('[@element_key] Unable to locate entity (@entity_id).', $t_args);
unset($values[$index]);
}
}
$values = array_values($values);
if (empty($values)) {
return $element_plugin
->hasMultipleValues($element) ? [] : NULL;
}
else {
return $element_plugin
->hasMultipleValues($element) ? $values : reset($values);
}
}
protected function importCompositeElement(array $element, $value, WebformSubmissionInterface $webform_submission = NULL, array &$errors) {
try {
return Yaml::decode($value);
} catch (\Exception $exception) {
$t_args = [
'@element_key' => $element['#webform_key'],
'@error' => $exception
->getMessage(),
];
$errors[] = $this
->t('[@element_key] YAML is not valid. @error', $t_args);
return [];
}
}
protected function importMultipleElement(array $element, $value, WebformSubmissionInterface $webform_submission = NULL, array &$errors) {
$values = preg_split('/\\s*,\\s*/', $value);
foreach ($values as $index => $item) {
$values[$index] = str_replace('%2C', ',', $item);
}
return $values;
}
protected function importValidateRecord(array $record) {
$values = $this
->importConvertRecordToValues($record);
return WebformSubmissionForm::validateFormValues($values);
}
protected function importSaveSubmission(array $record, WebformSubmissionInterface $webform_submission = NULL) {
$field_definitions = $this
->getFieldDefinitions();
$elements = $this
->getElements();
if ($webform_submission) {
unset($record['sid'], $record['serial'], $record['uuid']);
foreach ($record as $name => $value) {
if (isset($field_definitions[$name])) {
$webform_submission
->set($name, $value);
}
elseif (isset($elements[$name])) {
$webform_submission
->setElementData($name, $value);
}
}
}
else {
unset($record['sid'], $record['serial']);
$values = $this
->importConvertRecordToValues($record);
$webform_submission = $this
->getSubmissionStorage()
->create($values);
}
$webform_submission
->save();
}
protected function importConvertRecordToValues(array $record) {
$field_definitions = $this
->getFieldDefinitions();
$elements = $this
->getElements();
$values = [
'data' => [],
];
foreach ($record as $name => $value) {
if (isset($field_definitions[$name])) {
$values[$name] = $value;
}
elseif (isset($elements[$name])) {
$values['data'][$name] = $value;
}
}
unset($values['sid'], $values['serial']);
return $values;
}
public function getTotal() {
if (isset($this->importTotal)) {
return $this->importTotal;
}
$total = -1;
$handle = fopen($this->importUri, 'r');
while (!feof($handle)) {
$line = fgets($handle);
if (!empty(trim($line))) {
$total++;
}
}
$this->importTotal = $total;
return $this->importTotal;
}
public function getSourceColumns() {
$file = fopen($this
->getImportUri(), 'r');
$values = fgetcsv($file);
fclose($file);
return array_combine($values, $values);
}
public function getDestinationColumns() {
$columns = [];
$field_definitions = $this
->getFieldDefinitions();
foreach ($field_definitions as $field_name => $field_definition) {
$columns[$field_name] = $field_definition['title'];
}
$elements = $this
->getElements();
foreach ($elements as $element_key => $element) {
$element_plugin = $this->elementManager
->getElementInstance($element);
$element_title = $element_plugin
->getAdminLabel($element);
$has_multiple_values = $element_plugin
->hasMultipleValues($element);
if (!$has_multiple_values && $element_plugin instanceof WebformCompositeBase) {
$composite_elements = $element_plugin
->getCompositeElements();
foreach ($composite_elements as $composite_element_key => $composite_element) {
$composite_element_name = $element_key . '__' . $composite_element_key;
$composite_element_plugin = $this->elementManager
->getElementInstance($composite_element);
$composite_element_title = $composite_element_plugin
->getAdminLabel($composite_element);
$t_args = [
'@element_title' => $element_title,
'@composite_title' => $composite_element_title,
];
$columns[$composite_element_name] = $this
->t('@element_title: @composite_title', $t_args);
}
}
elseif (!$has_multiple_values && $element_plugin instanceof WebformLikert) {
$questions = $element['#questions'];
foreach ($questions as $question_key => $question) {
$question_element_name = $element_key . '__' . $question_key;
$t_args = [
'@element_title' => $element_title,
'@question_title' => $question,
];
$columns[$question_element_name] = $this
->t('@element_title: @question_title', $t_args);
}
}
else {
$columns[$element_key] = $element_title;
}
}
return $columns;
}
public function getSourceToDestinationColumnMapping() {
$source_column_names = $this
->getSourceColumns();
$destination_column_names = $this
->getDestinationColumns();
$mapping = [];
foreach ($source_column_names as $source_column_name) {
if (isset($destination_column_names[$source_column_name])) {
$mapping[$source_column_name] = $source_column_name;
}
else {
$mapping[$source_column_name] = '';
}
}
return $mapping;
}
public function getBatchLimit() {
return $this->configFactory
->get('webform.settings')
->get('batch.default_batch_import_size') ?: 100;
}
public function requiresBatch() {
return $this
->getTotal() > $this
->getBatchLimit() ? TRUE : FALSE;
}
protected function getEntityExportId(EntityInterface $entity = NULL, array $export_options = []) {
if (!$entity) {
return '';
}
else {
return empty($export_options['uuid']) ? $entity
->id() : $entity
->uuid();
}
}
protected function getEntityImportId($entity_type, $entity_id) {
if (!$this->entityTypeManager
->hasDefinition($entity_type)) {
return NULL;
}
$entity_storage = $this
->getEntityStorage($entity_type);
if ($entity_type === 'user') {
$properties = [
'uuid',
'mail',
'name',
];
}
else {
$properties = [
'uuid',
];
}
foreach ($properties as $property) {
$entities = $entity_storage
->loadByProperties([
$property => $entity_id,
]);
if ($entities) {
$entity = reset($entities);
return $entity
->id();
}
}
$entity = $entity_storage
->load($entity_id);
if ($entity) {
return $entity
->id();
}
return NULL;
}
protected function exportValue($value) {
if (is_string($value) && strpos($value, '+') === 0 || strpos($value, '-') === 0) {
return ' ' . $value;
}
else {
return $value;
}
}
}