From 0c6f3f58b4c336c62d2b1b120d370d5c01eaefa7 Mon Sep 17 00:00:00 2001 From: Chintan Desai Date: Tue, 15 Feb 2022 18:42:26 +0000 Subject: [PATCH] Adds module config form for admins. --- cdesai_migrate.links.menu.yml | 6 + cdesai_migrate.routing.yml | 7 + src/DemoMigrateConstantsInterface.php | 60 +++++ src/Form/CDesaiMigrateSettings.php | 333 ++++++++++++++++++++++++++ 4 files changed, 406 insertions(+) create mode 100644 cdesai_migrate.links.menu.yml create mode 100644 cdesai_migrate.routing.yml create mode 100644 src/DemoMigrateConstantsInterface.php create mode 100644 src/Form/CDesaiMigrateSettings.php diff --git a/cdesai_migrate.links.menu.yml b/cdesai_migrate.links.menu.yml new file mode 100644 index 0000000..4825bc7 --- /dev/null +++ b/cdesai_migrate.links.menu.yml @@ -0,0 +1,6 @@ +# Settings form +cdesai_migrate.settings: + title: 'Demo Migrations' + description: 'Configure Demo migration settings.' + route_name: cdesai_migrate.settings + parent: system.admin_config_development diff --git a/cdesai_migrate.routing.yml b/cdesai_migrate.routing.yml new file mode 100644 index 0000000..228206e --- /dev/null +++ b/cdesai_migrate.routing.yml @@ -0,0 +1,7 @@ +cdesai_migrate.settings: + path: '/admin/config/development/cdesai/migrate' + defaults: + _form: '\Drupal\cdesai_migrate\Form\CDesaiMigrateSettings' + _title: 'Demo Migrations' + requirements: + _permission: 'administer demo migrate' diff --git a/src/DemoMigrateConstantsInterface.php b/src/DemoMigrateConstantsInterface.php new file mode 100644 index 0000000..bc53cf7 --- /dev/null +++ b/src/DemoMigrateConstantsInterface.php @@ -0,0 +1,60 @@ + ['csv'], + 'archive' => ['zip'], + 'text' => ['txt'], + ]; + + const SOURCES = [ + 'cdesai_migrate_basic_pages_source' => [ + 'label' => 'Basic Pages Source', + 'directory' => 'private://migration/sources', + 'description' => 'File must be a CSV file.', + 'source_file_type' => 'spreadsheet', + 'required_columns' => [ + 'page', + 'skip_migration', + 'scrapped', + 'category', + 'title', + 'legacy_url', + ], + ], + 'cdesai_migrate_profile_pages_source' => [ + 'label' => 'Profile Pages Source', + 'directory' => 'private://migration/sources', + 'description' => 'File must be a CSV file.', + 'source_file_type' => 'spreadsheet', + 'required_columns' => [ + 'page', + 'skip_migration', + 'scrapped', + 'position', + 'title', + 'legacy_url', + 'year_birth', + 'year_death', + 'years_in_office', + 'agency', + 'image_url', + ], + ], + 'cdesai_migrate_paragraphs_source' => [ + 'label' => 'Paragraphs Source', + 'directory' => 'private://migration/sources', + 'description' => 'File must be a ZIP archive containing one or more .txt files.', + 'source_file_type' => 'archive', + 'archive_content_file_types' => 'text', + ], + ]; + +} diff --git a/src/Form/CDesaiMigrateSettings.php b/src/Form/CDesaiMigrateSettings.php new file mode 100644 index 0000000..20caa16 --- /dev/null +++ b/src/Form/CDesaiMigrateSettings.php @@ -0,0 +1,333 @@ +moduleHandler = $module_handler; + $this->fileSystem = $file_system; + $this->archiveManager = $archive_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('module_handler'), + $container->get('file_system'), + $container->get('plugin.manager.archiver') + ); + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames(): array { + return [ + DemoMigrateConstantsInterface::CONFIG_NAME, + ]; + } + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return self::FORM_ID; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + $configs = $this->config(DemoMigrateConstantsInterface::CONFIG_NAME); + + foreach (DemoMigrateConstantsInterface::SOURCES as $sourceKey => $source) { + + $currentPath = $configs->get($sourceKey . '_path'); + $text = $this->t('Upload a file'); + if ($currentPath && $this->validatePath($currentPath)) { + $text = $currentPath; + } + + $form[$sourceKey . '_current_path'] = [ + '#type' => 'markup', + '#markup' => + "Current {$source['label']} file: " . $text, + ]; + + $form[$sourceKey . '_upload'] = [ + '#type' => 'file', + '#title' => $this->t('Upload %title file', ['%title' => $source['label']]), + '#description' => $source['description'], + '#upload_validators' => [ + 'file_validate_extensions' => DemoMigrateConstantsInterface::FILE_TYPES[$source['source_file_type']], + ], + '#suffix' => '
', + ]; + } + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + if ($this->moduleHandler->moduleExists('file')) { + foreach (DemoMigrateConstantsInterface::SOURCES as $sourceKey => $source) { + // Validate file only if a new file was uploaded. + $upload_key = $sourceKey . '_upload'; + $file = _file_save_upload_from_form($form[$upload_key], $form_state, 0, FileSystemInterface::EXISTS_REPLACE); + if ($file) { + if ($source['source_file_type'] === 'archive') { + $contentFileExts = DemoMigrateConstantsInterface::FILE_TYPES[$source['archive_content_file_types']]; + $this->validateArchiveFile($upload_key, $contentFileExts, $file, $form_state); + } + else { + $this->validateCsvFile($upload_key, $source['required_columns'], $file, $form_state); + } + } + + } + + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + + $configs = $this->config(DemoMigrateConstantsInterface::CONFIG_NAME); + + // Exclude unnecessary elements before saving. + $form_state->cleanValues(); + $values = $form_state->getValues(); + + foreach (DemoMigrateConstantsInterface::SOURCES as $sourceKey => $source) { + // Check for a newly uploaded file. + $upload_key = $sourceKey . '_upload'; + $path_key = $sourceKey . '_path'; + + try { + if (isset($values[$upload_key]) && $values[$upload_key] instanceof File) { + $this->fileSystem->mkdir($source['directory'], NULL, TRUE); + $filename = $this->fileSystem->copy($values[$upload_key]->getFileUri(), $source['directory'], FileSystemInterface::EXISTS_RENAME); + $configs->set($path_key, $filename); + } + } + catch (FileException $e) { + // Ignore. + $this->logger('Demo Migration')->error($e->getMessage()); + $this->messenger()->addError($e->getMessage()); + } + + } + + $configs->save(); + } + + /** + * Helper function for the cdesai_migrate_settings form. + * + * Attempts to validate normal system paths, paths relative to the public + * files directory, or stream wrapper URIs. If the given path is any of the + * above, returns a valid path or URI that the theme system can display. + * + * @param string $path + * A path relative to the Drupal root or to the public files directory, or + * a stream wrapper URI. + * + * @return false|string + * A valid path that can be displayed through the theme system, or FALSE if + * the path could not be validated. + */ + protected function validatePath(string $path) { + // Absolute local file paths are invalid. + if ($this->fileSystem->realpath($path) == $path) { + return FALSE; + } + + // A path relative to the Drupal root or a fully qualified URI is valid. + if (is_file($path)) { + return $path; + } + + // Prepend 'private://' for relative file paths within private filesystem. + if (StreamWrapperManager::getScheme($path) === FALSE) { + $path = 'private://' . $path; + + if (is_file($path)) { + return $path; + } + } + + return FALSE; + } + + /** + * Validate the archive file. + * + * @param string $key + * Form field key. + * @param array $allowed_file_extensions + * File extensions allowed in the archive. + * @param \Drupal\file\FileInterface $file + * The file being uploaded. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + private function validateArchiveFile(string $key, array $allowed_file_extensions, FileInterface $file, FormStateInterface $form_state) { + $filepath = $this->fileSystem->realpath($file->getFileUri()); + /** @var \Drupal\Core\Archiver\ArchiverInterface $archiver */ + $archiver = $this->archiveManager->getInstance([ + 'filepath' => $filepath, + ]); + + if ($archiver && !empty($archiver->listContents())) { + $otherExt = FALSE; + foreach ($archiver->listContents() as $fileInArchive) { + $ext = pathinfo($fileInArchive, PATHINFO_EXTENSION); + if (!in_array($ext, $allowed_file_extensions)) { + $otherExt = TRUE; + break; + } + } + + if ($otherExt) { + $form_state->setErrorByName($key, $this->t('Archive contains one or more files with an invalid extension. Allowed extensions are: %ext', [ + '%ext' => implode(', ', $allowed_file_extensions), + ])); + } + else { + // Put the temporary file in form_values so we can save it + // on submit. + $form_state->setValue($key, $file); + } + } + else { + $form_state->setErrorByName($key, $this->t('Not a valid archive file.')); + } + } + + /** + * Validate the CSV file. + * + * @param string $key + * Form field key. + * @param array $required_columns + * An array column keys that should be in the CSV. + * @param \Drupal\file\FileInterface $file + * The file being uploaded. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + private function validateCsvFile(string $key, array $required_columns, FileInterface $file, FormStateInterface $form_state) { + $readFile = FALSE; + + if (($handle = fopen($file->getFileUri(), 'r')) !== FALSE) { + $row = fgetcsv($handle, 1000); + if ($row !== FALSE) { + $readFile = TRUE; + + // Sanitize keys in the header. + $header = array_map([__CLASS__, 'sanitizeKey'], $row); + // Check for required keys. + if ($required_columns) { + $missingRequiredKeys = array_diff($required_columns, $header); + if (!empty($missingRequiredKeys)) { + $form_state->setErrorByName($key, $this->t('CSV file is missing required columns: %columns', [ + '%columns' => implode(', ', $missingRequiredKeys), + ])); + } + else { + // Put the temporary file in form_values so we can save it + // on submit. + $form_state->setValue($key, $file); + } + } + } + + fclose($handle); + } + + if (!$readFile) { + $form_state->setErrorByName($key, $this->t('Unable to read from the file.')); + } + + } + + /** + * Remove whitespaces form a string and converts it to lowercase. + * + * @param string $key + * A string to sanitize. + * + * @return string + * A sanitized string. + */ + public static function sanitizeKey(string $key): string { + $key = trim($key); + $key = str_replace(' ', '_', $key); + return strtolower($key); + } + +}