diff --git a/core/modules/migrate_drupal/src/MigrationCreationTrait.php b/core/modules/migrate_drupal/src/MigrationCreationTrait.php new file mode 100644 index 0000000..e8777b7 --- /dev/null +++ b/core/modules/migrate_drupal/src/MigrationCreationTrait.php @@ -0,0 +1,136 @@ +getLegacyDrupalVersion($connection)) { + throw new \Exception($this->t('Source database does not contain a recognizable Drupal version.')); + } + + $version_tag = 'Drupal ' . $drupal_version; + + $template_storage = \Drupal::service('migrate.template_storage'); + $migration_templates = $template_storage->findTemplatesByTag($version_tag); + foreach ($migration_templates as $id => $template) { + // Configure file migrations so they can find the files. + if ($template['destination']['plugin'] == 'entity:file') { + if ($source_base_path) { + // Make sure we have a single trailing slash. + $source_base_path = rtrim($source_base_path, '/') . '/'; + $migration_templates[$id]['destination']['source_base_path'] = $source_base_path; + } + } + // @todo: Use a group to hold the db info, so we don't have to stuff it + // into every migration. + $migration_templates[$id]['source']['key'] = 'upgrade'; + $migration_templates[$id]['source']['database'] = $database; + } + + // Let the builder service create our migration configuration entities from + // the templates, expanding them to multiple entities where necessary. + /** @var \Drupal\migrate\MigrationBuilder $builder */ + $builder = \Drupal::service('migrate.migration_builder'); + $migrations = $builder->createMigrations($migration_templates); + $migration_ids = []; + foreach ($migrations as $migration) { + try { + if ($migration->getSourcePlugin() instanceof RequirementsInterface) { + $migration->getSourcePlugin()->checkRequirements(); + } + if ($migration->getDestinationPlugin() instanceof RequirementsInterface) { + $migration->getDestinationPlugin()->checkRequirements(); + } + $migration->save(); + $migration_ids[] = $migration->id(); + } + // Migrations which are not applicable given the source and destination + // site configurations (e.g., what modules are enabled) will be silently + // ignored. + catch (RequirementsException $e) { + } + catch (PluginNotFoundException $e) { + } + } + + // loadMultiple will sort the migrations in dependency order. + return array_keys(Migration::loadMultiple($migration_ids)); + } + + /** + * Determine what version of Drupal the source database contains. + * + * @param \Drupal\Core\Database\Connection $connection + * + * @return int|FALSE + */ + protected function getLegacyDrupalVersion(Connection $connection) { + // Don't assume because a table of that name exists, that it has the columns + // we're querying. Catch exceptions and report that the source database is + // not Drupal. + + // Druppal 5/6/7 can be detected by the schema_version in the system table. + if ($connection->schema()->tableExists('system')) { + try { + $version_string = $connection->query('SELECT schema_version FROM {system} WHERE name = :module', [':module' => 'system']) + ->fetchField(); + if ($version_string && $version_string[0] == '1') { + if ((int) $version_string >= 1000) { + $version_string = '5'; + } + else { + $version_string = FALSE; + } + } + } + catch (\PDOException $e) { + $version_string = FALSE; + } + } + // For Drupal 8 (and we're predicting beyond) the schema version is in the + // key_value store. + elseif ($connection->schema()->tableExists('key_value')) { + $result = $connection->query("SELECT value FROM {key_value} WHERE collection = :system_schema and name = :module", [':system_schema' => 'system.schema', ':module' => 'system'])->fetchField(); + $version_string = unserialize($result); + } + else { + $version_string = FALSE; + } + + return $version_string ? substr($version_string, 0, 1) : FALSE; + } + +} diff --git a/core/modules/migrate_drupal_ui/migrate_drupal_ui.info.yml b/core/modules/migrate_drupal_ui/migrate_drupal_ui.info.yml new file mode 100644 index 0000000..bc0ddd4 --- /dev/null +++ b/core/modules/migrate_drupal_ui/migrate_drupal_ui.info.yml @@ -0,0 +1,10 @@ +name: Migrate Drupal UI +type: module +description: 'UI for migration from older Drupal versions.' +package: Core +version: VERSION +core: 8.x +configure: migrate_drupal_ui.upgrade +dependencies: + - migrate + - migrate_drupal diff --git a/core/modules/migrate_drupal_ui/migrate_drupal_ui.install b/core/modules/migrate_drupal_ui/migrate_drupal_ui.install new file mode 100644 index 0000000..015e7c1 --- /dev/null +++ b/core/modules/migrate_drupal_ui/migrate_drupal_ui.install @@ -0,0 +1,16 @@ +toString(); + drupal_set_message(t("The Migrate Drupal UI module has been enabled. Proceed to the upgrade form.", array('@url' => $url))); +} diff --git a/core/modules/migrate_drupal_ui/migrate_drupal_ui.module b/core/modules/migrate_drupal_ui/migrate_drupal_ui.module new file mode 100755 index 0000000..a07f545 --- /dev/null +++ b/core/modules/migrate_drupal_ui/migrate_drupal_ui.module @@ -0,0 +1,19 @@ +' . t('This form is used for importing configuration and content from a previous version of your Drupal site. Make sure you have backups of your destination site before submitting this form. Also, ensure that the source database and files are accessible to the destination site. For more details, see upgrading from previous versions.', array('!migrate' => 'https://www.drupal.org/upgrade')) . '

'; + return $output; + } +} diff --git a/core/modules/migrate_drupal_ui/migrate_drupal_ui.routing.yml b/core/modules/migrate_drupal_ui/migrate_drupal_ui.routing.yml new file mode 100644 index 0000000..b8e522f --- /dev/null +++ b/core/modules/migrate_drupal_ui/migrate_drupal_ui.routing.yml @@ -0,0 +1,18 @@ +migrate_drupal_ui.upgrade: + path: '/upgrade' + defaults: + _form: '\Drupal\migrate_drupal_ui\Form\MigrateUpgradeForm' + _title: 'Migration' + requirements: + _permission: 'administer site configuration' + options: + _admin_route: TRUE + +migrate_drupal_ui.log: + path: '/upgrade/log' + defaults: + _controller: '\Drupal\migrate_drupal_ui\Controller\MigrateController::showLog' + requirements: + _permission: 'administer site configuration' + options: + _admin_route: TRUE diff --git a/core/modules/migrate_drupal_ui/src/Controller/MigrateController.php b/core/modules/migrate_drupal_ui/src/Controller/MigrateController.php new file mode 100644 index 0000000..254d8cd --- /dev/null +++ b/core/modules/migrate_drupal_ui/src/Controller/MigrateController.php @@ -0,0 +1,24 @@ + 'migrate_drupal_ui'); + return $this->redirect('dblog.overview'); + } +} diff --git a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php new file mode 100644 index 0000000..e736933 --- /dev/null +++ b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php @@ -0,0 +1,164 @@ +t('Drupal Upgrade: Source site information'); + + $form['database'] = array( + '#type' => 'details', + '#title' => $this->t('Source database'), + '#description' => $this->t('Provide credentials for the database of the Drupal site you want to migrate.'), + '#open' => TRUE, + ); + + // Copy the values from the parent form into our structure. + $form['database']['driver'] = $form['driver']; + $form['database']['settings'] = $form['settings']; + $form['database']['settings']['mysql']['host'] = $form['database']['settings']['mysql']['advanced_options']['host']; + $form['database']['settings']['mysql']['host']['#title'] = 'Database host'; + $form['database']['settings']['mysql']['host']['#weight'] = 0; + + // Remove the values from the parent form. + unset($form['driver']); + unset($form['database']['settings']['mysql']['database']['#default_value']); + unset($form['settings']); + unset($form['database']['settings']['mysql']['advanced_options']['host']); + + $form['source'] = array( + '#type' => 'details', + '#title' => $this->t('Source site files'), + '#open' => TRUE, + ); + $form['source']['source_base_path'] = array( + '#type' => 'textfield', + '#title' => $this->t('Source file path'), + '#default_value' => 'http://', + '#description' => $this->t('To import public files from your current Drupal site, enter your site address (e.g. http://example.com), or a local file directory containing your site (e.g. /var/www/docroot).'), + ); + +/* + $form['files'] = array( + '#type' => 'details', + '#title' => $this->t('Files'), + '#open' => TRUE, + '#weight' => 2, + ); + // @todo: Not yet implemented, depends on https://www.drupal.org/node/2547125. + $form['files']['private_file_directory'] = array( + '#type' => 'textfield', + '#title' => $this->t('Private file path'), + '#description' => $this->t('To import private files from your current Drupal site, enter a local file directory containing your files (e.g. /var/private_files).'), + ); +*/ + + // Rename the submit button. + $form['actions']['save']['#value'] = $this->t('Perform upgrade'); + + // The parent form uses #limit_validation_errors to avoid validating the + // unselected database drivers. This makes it difficult for us to handle + // database errors in our validation, and does not appear to actually be + // necessary with the current implementation, so we remove it. + unset($form['actions']['save']['#limit_validation_errors']); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + // Retrieve the database driver from the form, use reflection to get the + // namespace and then construct a valid database array the same as in + // settings.php. + $driver = $form_state->getValue('driver'); + $drivers = $this->getDatabaseTypes(); + $reflection = new \ReflectionClass($drivers[$driver]); + $install_namespace = $reflection->getNamespaceName(); + + $database = $form_state->getValue($driver); + // Cut the trailing \Install from namespace. + $database['namespace'] = substr($install_namespace, 0, strrpos($install_namespace, '\\')); + $database['driver'] = $driver; + + // Validate the driver settings and just end here if we have any issues. + if ($errors = $drivers[$driver]->validateDatabaseSettings($database)) { + foreach ($errors as $name => $message) { + $form_state->setErrorByName($name, $message); + } + return; + } + + try { + // Create all the relevant migrations and get their IDs so we can run them. + $migration_ids = $this->createMigrations($database, $form_state->getValue('source_base_path')); + + // Store the retrieved migration ids on the form state. + $form_state->setValue('migration_ids', $migration_ids); + } + catch (\Exception $e) { + $form_state->setErrorByName(NULL, $this->t($e->getMessage())); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $batch = array( + 'title' => $this->t('Running migrations'), + 'progress_message' => '', + 'operations' => array( + array(array('Drupal\migrate_drupal_ui\MigrateUpgradeRunBatch', 'run'), array($form_state->getValue('migration_ids'))), + ), + 'finished' => array('Drupal\migrate_drupal_ui\MigrateUpgradeRunBatch', 'finished'), + ); + batch_set($batch); + $form_state->setRedirect(''); + } + + /** + * Returns all supported database driver installer objects. + * + * @return \Drupal\Core\Database\Install\Tasks[] + * An array of available database driver installer objects. + */ + protected function getDatabaseTypes() { + // Make sure the install API is available. + include_once DRUPAL_ROOT . '/core/includes/install.core.inc'; + return drupal_get_database_types(); + } + +} diff --git a/core/modules/migrate_drupal_ui/src/MigrateMessageCapture.php b/core/modules/migrate_drupal_ui/src/MigrateMessageCapture.php new file mode 100644 index 0000000..138613f --- /dev/null +++ b/core/modules/migrate_drupal_ui/src/MigrateMessageCapture.php @@ -0,0 +1,48 @@ +messages[] = $message; + } + + /** + * Clear out any captured messages. + */ + public function clear() { + $this->messages = []; + } + + /** + * Return any captured messages. + * + * @return array + */ + public function getMessages() { + return $this->messages; + } + +} diff --git a/core/modules/migrate_drupal_ui/src/MigrateUpgradeRunBatch.php b/core/modules/migrate_drupal_ui/src/MigrateUpgradeRunBatch.php new file mode 100644 index 0000000..184647b --- /dev/null +++ b/core/modules/migrate_drupal_ui/src/MigrateUpgradeRunBatch.php @@ -0,0 +1,173 @@ +label() ? $migration->label() : $migration_id; + static::logger()->notice('Importing @migration', array('@migration' => $migration_name)); + + try { + $migration_status = $executable->import(); + } + catch (\Exception $e) { + // PluginNotFoundException is when the D8 module is disabled, maybe that + // should be a RequirementsException instead. + static::logger()->error($e->getMessage()); + $migration_status = MigrationInterface::RESULT_FAILED; + } + + switch ($migration_status) { + case MigrationInterface::RESULT_COMPLETED: + $context['sandbox']['messages'][] = t('Imported @migration', array('@migration' => $migration_name)); + $context['results']['successes']++; + static::logger()->notice('Imported @migration', array('@migration' => $migration_name)); + break; + + case MigrationInterface::RESULT_INCOMPLETE: + $context['sandbox']['messages'][] = t('Importing @migration', array('@migration' => $migration_name)); + break; + + case MigrationInterface::RESULT_STOPPED: + $context['sandbox']['messages'][] = t('Import stopped by request'); + break; + + case MigrationInterface::RESULT_FAILED: + $context['sandbox']['messages'][] = t('Import of @migration failed', array('@migration' => $migration_name)); + $context['results']['failures']++; + static::logger()->error('Import of @migration failed', array('@migration' => $migration_name)); + break; + + case MigrationInterface::RESULT_SKIPPED: + $context['sandbox']['messages'][] = t('Import of @migration skipped due to unfulfilled dependencies', array('@migration' => $migration_name)); + static::logger()->error('Import of @migration skipped due to unfulfilled dependencies', array('@migration' => $migration_name)); + break; + + case MigrationInterface::RESULT_DISABLED: + // Skip silently if disabled. + break; + } + + // Unless we're continuing on with this migration, take it off the list. + if ($migration_status != MigrationInterface::RESULT_INCOMPLETE) { + array_shift($context['sandbox']['migration_ids']); + } + + // Add any captured messages. + foreach ($messages->getMessages() as $message) { + $context['sandbox']['messages'][] = $message; + } + + // Only display the last 10 messages, in reverse order. + $message_count = count($context['sandbox']['messages']); + $context['message'] = ''; + for ($index = max(0, $message_count - 10); $index < $message_count; $index++) { + $context['message'] = $context['sandbox']['messages'][$index]. "
\n" . $context['message']; + } + if ($message_count > 10) { + // Indicate there are earlier messages not displayed. + $context['message'] .= '…'; + } + // At the top of the list, display the next one (which will be the one + // that is running while this message is visible). + if (!empty($context['sandbox']['migration_ids'])) { + $migration_id = reset($context['sandbox']['migration_ids']); + $migration = Migration::load($migration_id); + $migration_name = $migration->label() ? $migration->label() : $migration_id; + $context['message'] = t('Currently importing @migration', array('@migration' => $migration_name)) + . "
\n" . $context['message']; + } + } + else { + array_shift($context['sandbox']['migration_ids']); + } + + $context['finished'] = 1 - count($context['sandbox']['migration_ids']) / $context['sandbox']['max']; + } + + /** + * A helper method to grab the logger using the migrate_drupal_ui channel. + * + * @return \Psr\Log\LoggerInterface + * The logger instance. + */ + protected static function logger() { + return \Drupal::logger('migrate_drupal_ui'); + } + + /** + * Implementation of the Batch API finished method. + */ + public static function finished($success, $results, $operations, $elapsed) { + static::displayResults($results); + } + + /** + * Display counts of success/failures on the migration upgrade complete page. + * + * @param $results + * An array of result data built during the batch. + */ + protected static function displayResults($results) { + $successes = $results['successes']; + $failures = $results['failures']; + $translation = \Drupal::translation(); + + // If we had any successes lot that for the user. + if ($successes > 0) { + drupal_set_message(t('Import completed @count successfully.', array('@count' => $translation->formatPlural($successes, '1 migration', '@count migrations')))); + } + + // If we had failures, log them and show the migration failed. + if ($failures > 0) { + drupal_set_message(t('@count failed', array('@count' => $translation->formatPlural($failures, '1 migration', '@count migrations'))), 'error'); + drupal_set_message(t('Import process not completed'), 'error'); + } + else { + // Everything went off without a hitch. We may not have had successes but + // we didn't have failures so this is fine. + drupal_set_message(t('Congratulations, you upgraded Drupal!')); + } + + if (\Drupal::moduleHandler()->moduleExists('dblog')) { + $url = Url::fromRoute('migrate_drupal_ui.log'); + drupal_set_message(\Drupal::l(t('Review the detailed migration log'), $url), $failures ? 'error' : 'status'); + } + } + +}