diff --git a/core/composer.json b/core/composer.json index dbe1db9..f88d372 100644 --- a/core/composer.json +++ b/core/composer.json @@ -101,6 +101,7 @@ "drupal/menu_ui": "self.version", "drupal/migrate": "self.version", "drupal/migrate_drupal": "self.version", + "drupal/migrate_drupal_ui": "self.version", "drupal/node": "self.version", "drupal/options": "self.version", "drupal/page_cache": "self.version", diff --git a/core/modules/migrate_drupal/src/MigrationCreationTrait.php b/core/modules/migrate_drupal/src/MigrationCreationTrait.php new file mode 100644 index 0000000..fa08e4b --- /dev/null +++ b/core/modules/migrate_drupal/src/MigrationCreationTrait.php @@ -0,0 +1,225 @@ +getConnection($database); + $system_data = []; + try { + $results = $connection->select('system', 's', [ + 'fetch' => \PDO::FETCH_ASSOC, + ]) + ->fields('s') + ->execute(); + foreach ($results as $result) { + $system_data[$result['type']][$result['name']] = $result; + } + } + catch (\Exception $e) { + // The table might not exist for example in tests. + } + return $system_data; + } + + /** + * Sets up the relevant migrations for import from a database connection. + * + * @param array $database + * Database array representing the source Drupal database. + * @param string $source_base_path + * (Optional) Address of the source Drupal site (e.g., http://example.com/). + * + * @return array + * An array of the migration templates (parsed YAML config arrays) that were + * tagged for the identified source Drupal version. The templates are + * populated with database state key and file source base path information + * for execution. The array is keyed by migration IDs. + * + * @throws \Exception + */ + protected function getMigrationTemplates(array $database, $source_base_path = '') { + // Set up the connection. + $connection = $this->getConnection($database); + if (!$drupal_version = $this->getLegacyDrupalVersion($connection)) { + throw new \Exception('Source database does not contain a recognizable Drupal version.'); + } + $database_state['key'] = 'upgrade'; + $database_state['database'] = $database; + $database_state_key = 'migrate_drupal_' . $drupal_version; + \Drupal::state()->set($database_state_key, $database_state); + + $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) { + $migration_templates[$id]['source']['database_state_key'] = $database_state_key; + // 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; + } + } + } + return $migration_templates; + } + + /** + * Gets the migrations for import. + * + * Uses the migration template connection to ensure that only the relevant + * migrations are returned. + * + * @param array $migration_templates + * Migration templates (parsed YAML config arrays), keyed by the ID. + * + * @return \Drupal\migrate\Entity\MigrationInterface[] + * The migrations for import. + */ + protected function getMigrations(array $migration_templates) { + // 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'); + $initial_migrations = $builder->createMigrations($migration_templates); + $migrations = []; + foreach ($initial_migrations as $migration) { + try { + $source_plugin = $migration->getSourcePlugin(); + if ($source_plugin instanceof RequirementsInterface) { + $source_plugin->checkRequirements(); + } + $destination_plugin = $migration->getDestinationPlugin(); + if ($destination_plugin instanceof RequirementsInterface) { + $destination_plugin->checkRequirements(); + } + $migrations[] = $migration; + } + // 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) { + } + } + + return $migrations; + } + + /** + * Saves the migrations for import from the provided template connection. + * + * @param array $migration_templates + * Migration template. + * + * @return array + * The migration IDs sorted in dependency order. + */ + protected function createMigrations(array $migration_templates) { + $migration_ids = []; + $migrations = $this->getMigrations($migration_templates); + foreach ($migrations as $migration) { + // Don't try to resave migrations that already exist. + if (!Migration::load($migration->id())) { + $migration->save(); + } + $migration_ids[] = $migration->id(); + } + // loadMultiple will sort the migrations in dependency order. + return array_keys(Migration::loadMultiple($migration_ids)); + } + + /** + * Determines what version of Drupal the source database contains. + * + * @param \Drupal\Core\Database\Connection $connection + * The database connection object. + * + * @return int|FALSE + * An integer representing the major branch of Drupal core (e.g. '6' for + * Drupal 6.x), or FALSE if no valid version is matched. + */ + 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. + // Drupal 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..4a9c448 --- /dev/null +++ b/core/modules/migrate_drupal_ui/migrate_drupal_ui.info.yml @@ -0,0 +1,11 @@ +name: 'Drupal Upgrade UI' +type: module +description: 'UI for direct upgrades from older Drupal versions.' +package: 'Core (Experimental)' +version: VERSION +core: 8.x +configure: migrate_drupal_ui.upgrade +dependencies: + - migrate + - migrate_drupal + - dblog 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..8fb0783 --- /dev/null +++ b/core/modules/migrate_drupal_ui/migrate_drupal_ui.install @@ -0,0 +1,16 @@ +toString(); + drupal_set_message(t('The Drupal Upgrade module has been enabled. Proceed to the upgrade form.', [':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 100644 index 0000000..6202081 --- /dev/null +++ b/core/modules/migrate_drupal_ui/migrate_drupal_ui.module @@ -0,0 +1,19 @@ +' . t('The Drupal Upgrade UI module provides a one-click upgrade from an earlier version of Drupal. For details, see the online documentation for the Drupal Upgrade UI module in the handbook on upgrading from previous versions.', [':migrate' => 'https://www.drupal.org/upgrade/migrate']) . '

'; + 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..4b4bd08 --- /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: 'Upgrade' + requirements: + _permission: 'administer software updates' + options: + _admin_route: TRUE + +migrate_drupal_ui.log: + path: '/upgrade/log' + defaults: + _controller: '\Drupal\migrate_drupal_ui\Controller\MigrateController::showLog' + requirements: + _permission: 'administer software updates' + 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..5d4a885 --- /dev/null +++ b/core/modules/migrate_drupal_ui/src/Controller/MigrateController.php @@ -0,0 +1,29 @@ + '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..f910601 --- /dev/null +++ b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php @@ -0,0 +1,1232 @@ + [ + 'source_module' => 'system', + 'destination_module' => 'action', + ], + 'd6_aggregator_feed' => [ + 'source_module' => 'aggregator', + 'destination_module' => 'aggregator', + ], + 'd6_aggregator_item' => [ + 'source_module' => 'aggregator', + 'destination_module' => 'aggregator', + ], + 'd6_aggregator_settings' => [ + 'source_module' => 'aggregator', + 'destination_module' => 'aggregator', + ], + 'd7_aggregator_feed' => [ + 'source_module' => 'aggregator', + 'destination_module' => 'aggregator', + ], + 'd7_aggregator_item' => [ + 'source_module' => 'aggregator', + 'destination_module' => 'aggregator', + ], + 'd7_aggregator_settings' => [ + 'source_module' => 'aggregator', + 'destination_module' => 'aggregator', + ], + 'd7_blocked_ips' => [ + 'source_module' => 'system', + 'destination_module' => 'ban', + ], + 'd6_block' => [ + 'source_module' => 'block', + 'destination_module' => 'block', + ], + 'd7_block' => [ + 'source_module' => 'block', + 'destination_module' => 'block', + ], + 'block_content_body_field' => [ + 'source_module' => 'block', + 'destination_module' => 'block_content', + ], + 'block_content_type' => [ + 'source_module' => 'block', + 'destination_module' => 'block_content', + ], + 'd6_custom_block' => [ + 'source_module' => 'block', + 'destination_module' => 'block_content', + ], + 'd7_custom_block' => [ + 'source_module' => 'block', + 'destination_module' => 'block_content', + ], + 'd6_book' => [ + 'source_module' => 'book', + 'destination_module' => 'book', + ], + 'd6_book_settings' => [ + 'source_module' => 'book', + 'destination_module' => 'book', + ], + 'd6_comment' => [ + 'source_module' => 'comment', + 'destination_module' => 'comment', + ], + 'd6_comment_entity_display' => [ + 'source_module' => 'comment', + 'destination_module' => 'comment', + ], + 'd6_comment_entity_form_display' => [ + 'source_module' => 'comment', + 'destination_module' => 'comment', + ], + 'd6_comment_entity_form_display_subject' => [ + 'source_module' => 'comment', + 'destination_module' => 'comment', + ], + 'd6_comment_field' => [ + 'source_module' => 'comment', + 'destination_module' => 'comment', + ], + 'd6_comment_field_instance' => [ + 'source_module' => 'comment', + 'destination_module' => 'comment', + ], + 'd6_comment_type' => [ + 'source_module' => 'comment', + 'destination_module' => 'comment', + ], + 'd7_comment' => [ + 'source_module' => 'comment', + 'destination_module' => 'comment', + ], + 'd7_comment_entity_display' => [ + 'source_module' => 'comment', + 'destination_module' => 'comment', + ], + 'd7_comment_entity_form_display' => [ + 'source_module' => 'comment', + 'destination_module' => 'comment', + ], + 'd7_comment_entity_form_display_subject' => [ + 'source_module' => 'comment', + 'destination_module' => 'comment', + ], + 'd7_comment_field' => [ + 'source_module' => 'comment', + 'destination_module' => 'comment', + ], + 'd7_comment_field_instance' => [ + 'source_module' => 'comment', + 'destination_module' => 'comment', + ], + 'd7_comment_type' => [ + 'source_module' => 'comment', + 'destination_module' => 'comment', + ], + 'contact_category' => [ + 'source_module' => 'contact', + 'destination_module' => 'contact', + ], + 'd6_contact_settings' => [ + 'source_module' => 'contact', + 'destination_module' => 'contact', + ], + 'd7_contact_settings' => [ + 'source_module' => 'contact', + 'destination_module' => 'contact', + ], + 'd6_dblog_settings' => [ + 'source_module' => 'dblog', + 'destination_module' => 'dblog', + ], + 'd7_dblog_settings' => [ + 'source_module' => 'dblog', + 'destination_module' => 'dblog', + ], + 'd6_field' => [ + 'source_module' => 'content', + 'destination_module' => 'field', + ], + 'd6_field_formatter_settings' => [ + 'source_module' => 'content', + 'destination_module' => 'field', + ], + 'd6_field_instance' => [ + 'source_module' => 'content', + 'destination_module' => 'field', + ], + 'd6_field_instance_widget_settings' => [ + 'source_module' => 'content', + 'destination_module' => 'field', + ], + 'd7_field' => [ + 'source_module' => 'field', + 'destination_module' => 'field', + ], + 'd7_field_formatter_settings' => [ + 'source_module' => 'field', + 'destination_module' => 'field', + ], + 'd7_field_instance' => [ + 'source_module' => 'field', + 'destination_module' => 'field', + ], + 'd7_field_instance_widget_settings' => [ + 'source_module' => 'field', + 'destination_module' => 'field', + ], + 'd7_view_modes' => [ + 'source_module' => 'field', + 'destination_module' => 'field', + ], + 'd6_file' => [ + 'source_module' => 'system', + 'destination_module' => 'file', + ], + 'd6_file_settings' => [ + 'source_module' => 'system', + 'destination_module' => 'file', + ], + 'd6_upload' => [ + 'source_module' => 'upload', + 'destination_module' => 'file', + ], + 'd6_upload_entity_display' => [ + 'source_module' => 'upload', + 'destination_module' => 'file', + ], + 'd6_upload_entity_form_display' => [ + 'source_module' => 'upload', + 'destination_module' => 'file', + ], + 'd6_upload_field' => [ + 'source_module' => 'upload', + 'destination_module' => 'file', + ], + 'd6_upload_field_instance' => [ + 'source_module' => 'upload', + 'destination_module' => 'file', + ], + 'd7_file' => [ + 'source_module' => 'file', + 'destination_module' => 'file', + ], + 'd6_filter_format' => [ + 'source_module' => 'filter', + 'destination_module' => 'filter', + ], + 'd7_filter_format' => [ + 'source_module' => 'filter', + 'destination_module' => 'filter', + ], + 'd6_forum_settings' => [ + 'source_module' => 'forum', + 'destination_module' => 'forum', + ], + 'd7_forum_settings' => [ + 'source_module' => 'forum', + 'destination_module' => 'forum', + ], + 'd6_imagecache_presets' => [ + 'source_module' => 'imagecache', + 'destination_module' => 'image', + ], + 'd7_image_settings' => [ + 'source_module' => 'image', + 'destination_module' => 'image', + ], + 'd7_image_styles' => [ + 'source_module' => 'image', + 'destination_module' => 'image', + ], + 'd7_language_negotiation_settings' => [ + 'source_module' => 'locale', + 'destination_module' => 'language', + ], + 'language' => [ + 'source_module' => 'locale', + 'destination_module' => 'language', + ], + 'locale_settings' => [ + 'source_module' => 'locale', + 'destination_module' => 'locale', + ], + 'menu_links' => [ + 'source_module' => 'menu', + 'destination_module' => 'menu_link_content', + ], + 'menu_settings' => [ + 'source_module' => 'menu', + 'destination_module' => 'menu_ui', + ], + 'd6_node' => [ + 'source_module' => 'node', + 'destination_module' => 'node', + ], + 'd6_node_revision' => [ + 'source_module' => 'node', + 'destination_module' => 'node', + ], + 'd6_node_setting_promote' => [ + 'source_module' => 'node', + 'destination_module' => 'node', + ], + 'd6_node_setting_status' => [ + 'source_module' => 'node', + 'destination_module' => 'node', + ], + 'd6_node_setting_sticky' => [ + 'source_module' => 'node', + 'destination_module' => 'node', + ], + 'd6_node_settings' => [ + 'source_module' => 'node', + 'destination_module' => 'node', + ], + 'd6_node_type' => [ + 'source_module' => 'node', + 'destination_module' => 'node', + ], + 'd6_view_modes' => [ + 'source_module' => 'node', + 'destination_module' => 'node', + ], + 'd7_node' => [ + 'source_module' => 'node', + 'destination_module' => 'node', + ], + 'd7_node_revision' => [ + 'source_module' => 'node', + 'destination_module' => 'node', + ], + 'd7_node_settings' => [ + 'source_module' => 'node', + 'destination_module' => 'node', + ], + 'd7_node_title_label' => [ + 'source_module' => 'node', + 'destination_module' => 'node', + ], + 'd7_node_type' => [ + 'source_module' => 'node', + 'destination_module' => 'node', + ], + 'd6_url_alias' => [ + 'source_module' => 'path', + 'destination_module' => 'path', + ], + 'd7_url_alias' => [ + 'source_module' => 'path', + 'destination_module' => 'path', + ], + 'search_page' => [ + 'source_module' => 'search', + 'destination_module' => 'search', + ], + 'd6_search_settings' => [ + 'source_module' => 'search', + 'destination_module' => 'search', + ], + 'd7_search_settings' => [ + 'source_module' => 'search', + 'destination_module' => 'search', + ], + 'd7_shortcut' => [ + 'source_module' => 'shortcut', + 'destination_module' => 'shortcut', + ], + 'd7_shortcut_set' => [ + 'source_module' => 'shortcut', + 'destination_module' => 'shortcut', + ], + 'd7_shortcut_set_users' => [ + 'source_module' => 'shortcut', + 'destination_module' => 'shortcut', + ], + 'd6_simpletest_settings' => [ + 'source_module' => 'simpletest', + 'destination_module' => 'simpletest', + ], + 'd7_simpletest_settings' => [ + 'source_module' => 'simpletest', + 'destination_module' => 'simpletest', + ], + 'd6_statistics_settings' => [ + 'source_module' => 'statistics', + 'destination_module' => 'statistics', + ], + 'd6_syslog_settings' => [ + 'source_module' => 'syslog', + 'destination_module' => 'syslog', + ], + 'd7_syslog_settings' => [ + 'source_module' => 'syslog', + 'destination_module' => 'syslog', + ], + 'd6_date_formats' => [ + 'source_module' => 'system', + 'destination_module' => 'system', + ], + 'd6_system_cron' => [ + 'source_module' => 'system', + 'destination_module' => 'system', + ], + 'd6_system_date' => [ + 'source_module' => 'system', + 'destination_module' => 'system', + ], + 'd6_system_file' => [ + 'source_module' => 'system', + 'destination_module' => 'system', + ], + 'd6_system_image' => [ + 'source_module' => 'system', + 'destination_module' => 'system', + ], + 'd6_system_image_gd' => [ + 'source_module' => 'system', + 'destination_module' => 'system', + ], + 'd6_system_logging' => [ + 'source_module' => 'system', + 'destination_module' => 'system', + ], + 'd6_system_maintenance' => [ + 'source_module' => 'system', + 'destination_module' => 'system', + ], + 'd6_system_performance' => [ + 'source_module' => 'system', + 'destination_module' => 'system', + ], + 'd6_system_rss' => [ + 'source_module' => 'system', + 'destination_module' => 'system', + ], + 'd6_system_site' => [ + 'source_module' => 'system', + 'destination_module' => 'system', + ], + 'menu' => [ + 'source_module' => 'menu', + 'destination_module' => 'system', + ], + 'taxonomy_settings' => [ + 'source_module' => 'taxonomy', + 'destination_module' => 'taxonomy', + ], + 'd6_taxonomy_term' => [ + 'source_module' => 'taxonomy', + 'destination_module' => 'taxonomy', + ], + 'd6_taxonomy_vocabulary' => [ + 'source_module' => 'taxonomy', + 'destination_module' => 'taxonomy', + ], + 'd6_term_node' => [ + 'source_module' => 'taxonomy', + 'destination_module' => 'taxonomy', + ], + 'd6_term_node_revision' => [ + 'source_module' => 'taxonomy', + 'destination_module' => 'taxonomy', + ], + 'd6_vocabulary_entity_display' => [ + 'source_module' => 'taxonomy', + 'destination_module' => 'taxonomy', + ], + 'd6_vocabulary_entity_form_display' => [ + 'source_module' => 'taxonomy', + 'destination_module' => 'taxonomy', + ], + 'd6_vocabulary_field' => [ + 'source_module' => 'taxonomy', + 'destination_module' => 'taxonomy', + ], + 'd6_vocabulary_field_instance' => [ + 'source_module' => 'taxonomy', + 'destination_module' => 'taxonomy', + ], + 'd7_taxonomy_term' => [ + 'source_module' => 'taxonomy', + 'destination_module' => 'taxonomy', + ], + 'd7_taxonomy_vocabulary' => [ + 'source_module' => 'taxonomy', + 'destination_module' => 'taxonomy', + ], + 'text_settings' => [ + 'source_module' => 'text', + 'destination_module' => 'text', + ], + 'd7_tracker_node' => [ + 'source_module' => 'tracker', + 'destination_module' => 'tracker', + ], + 'd7_tracker_settings' => [ + 'source_module' => 'tracker', + 'destination_module' => 'tracker', + ], + 'd7_tracker_user' => [ + 'source_module' => 'tracker', + 'destination_module' => 'tracker', + ], + 'update_settings' => [ + 'source_module' => 'update', + 'destination_module' => 'update', + ], + 'd6_profile_values' => [ + 'source_module' => 'profile', + 'destination_module' => 'user', + ], + 'd6_user' => [ + 'source_module' => 'user', + 'destination_module' => 'user', + ], + 'd6_user_contact_settings' => [ + 'source_module' => 'user', + 'destination_module' => 'user', + ], + 'd6_user_mail' => [ + 'source_module' => 'user', + 'destination_module' => 'user', + ], + 'd6_user_picture_file' => [ + 'source_module' => 'user', + 'destination_module' => 'user', + ], + 'd6_user_role' => [ + 'source_module' => 'user', + 'destination_module' => 'user', + ], + 'd6_user_settings' => [ + 'source_module' => 'user', + 'destination_module' => 'user', + ], + 'd7_user' => [ + 'source_module' => 'user', + 'destination_module' => 'user', + ], + 'd7_user_flood' => [ + 'source_module' => 'user', + 'destination_module' => 'user', + ], + 'd7_user_mail' => [ + 'source_module' => 'user', + 'destination_module' => 'user', + ], + 'd7_user_role' => [ + 'source_module' => 'user', + 'destination_module' => 'user', + ], + 'user_picture_entity_display' => [ + 'source_module' => 'user', + 'destination_module' => 'user', + ], + 'user_picture_entity_form_display' => [ + 'source_module' => 'user', + 'destination_module' => 'user', + ], + 'user_picture_field' => [ + 'source_module' => 'user', + 'destination_module' => 'user', + ], + 'user_picture_field_instance' => [ + 'source_module' => 'user', + 'destination_module' => 'user', + ], + 'user_profile_entity_display' => [ + 'source_module' => 'profile', + 'destination_module' => 'user', + ], + 'user_profile_entity_form_display' => [ + 'source_module' => 'profile', + 'destination_module' => 'user', + ], + 'user_profile_field' => [ + 'source_module' => 'profile', + 'destination_module' => 'user', + ], + 'user_profile_field_instance' => [ + 'source_module' => 'profile', + 'destination_module' => 'user', + ], + ]; + + /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + + /** + * The date formatter service. + * + * @var \Drupal\Core\Datetime\DateFormatterInterface + */ + protected $dateFormatter; + + /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * The migration entity storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $entityStorage; + + /** + * Constructs the MigrateUpgradeForm. + * + * @param \Drupal\Core\State\StateInterface $state + * The state service. + * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter + * The date formatter service. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. + * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage + * The migration entity storage. + */ + public function __construct(StateInterface $state, DateFormatterInterface $date_formatter, RendererInterface $renderer, EntityStorageInterface $entity_storage) { + $this->state = $state; + $this->dateFormatter = $date_formatter; + $this->renderer = $renderer; + $this->entityStorage = $entity_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('state'), + $container->get('date.formatter'), + $container->get('renderer'), + $container->get('entity_type.manager')->getStorage('migration') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'migrate_drupal_ui_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $step = $form_state->getValue('step', 'overview'); + switch ($step) { + case 'overview': + return $this->buildOverviewForm($form, $form_state); + + case 'credentials': + return $this->buildCredentialForm($form, $form_state); + + case 'confirm': + return $this->buildConfirmForm($form, $form_state); + + default: + drupal_set_message($this->t('Unrecognized form step @step', ['@step' => $step]), 'error'); + return []; + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // This method is intentionally empty, see the specific submit methods for + // each form step. + } + + /** + * Builds the form presenting an overview of the migration process. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * The form structure. + */ + public function buildOverviewForm(array $form, FormStateInterface $form_state) { + $form['#title'] = $this->t('Drupal Upgrade'); + + if ($date_performed = $this->state->get('migrate_drupal_ui.performed')) { + $form['upgrade_option_item'] = [ + '#type' => 'item', + '#prefix' => $this->t('

An upgrade has already been performed on this site.

'), + '#description' => $this->t('

Last upgrade: @date

', ['@date' => $this->dateFormatter->format($date_performed)]), + ]; + $form['upgrade_option'] = array( + '#type' => 'radios', + '#title' => $this->t('You have two options:'), + '#default_value' => static::MIGRATE_UPGRADE_INCREMENTAL, + '#options' => [ + static::MIGRATE_UPGRADE_INCREMENTAL => $this->t('Rerun: Import additional configuration and content that was not available when running the upgrade previously.'), + static::MIGRATE_UPGRADE_ROLLBACK => $this->t('Rollback: Remove content and configuration entities (such as fields and node types). Default values of other configuration will not be reverted (such as site name).'), + ], + ); + $validate = ['::validateCredentialForm']; + } + else { + $form['info_header'] = [ + '#markup' => '

' . $this->t('Upgrade a Drupal site by importing it into a clean and empty new install of Drupal 8. You will lose any existing configuration once you import your site into it. See the upgrading handbook for more detailed information.', [ + ':url' => 'https://www.drupal.org/upgrade/migrate', + ]), + ]; + + $info[] = $this->t('Back up the database for this site. Upgrade will change the database for this site.'); + $info[] = $this->t('Make sure that the host this site is on has access to the database for your previous site.'); + $info[] = $this->t('If your previous site has private files to be migrated, a copy of your files directory must be accessible on the host this site is on.'); + $info[] = $this->t('In general, enable all modules on this site that are enabled on the previous site. For example, if you have used the book module on the previous site then you must enable the book module on this site for that data to be available on this site.'); + $info[] = $this->t('Put this site into maintenance mode.', [ + ':url' => Url::fromRoute('system.site_maintenance_mode')->toString(TRUE)->getGeneratedUrl(), + ]); + + $form['info'] = [ + '#theme' => 'item_list', + '#list_type' => 'ol', + '#items' => $info, + ]; + + $form['info_footer'] = [ + '#markup' => '

' . $this->t('This upgrade can take a long time. It is better to import a local copy of your site instead of directly importing from your live site.'), + ]; + + $validate = []; + } + + $form['actions'] = ['#type' => 'actions']; + $form['actions']['save'] = [ + '#type' => 'submit', + '#value' => $this->t('Continue'), + '#button_type' => 'primary', + '#validate' => $validate, + '#submit' => ['::submitOverviewForm'], + ]; + return $form; + } + + /** + * Form submission handler for the overview form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function submitOverviewForm(array &$form, FormStateInterface $form_state) { + switch ($form_state->getValue('upgrade_option')) { + case static::MIGRATE_UPGRADE_INCREMENTAL: + $form_state->setValue('step', 'confirm'); + break; + + case static::MIGRATE_UPGRADE_ROLLBACK: + $form_state->setValue('step', 'confirm'); + break; + + default: + $form_state->setValue('step', 'credentials'); + break; + } + $form_state->setRebuild(); + } + + /** + * Builds the database credential form and adds file location information. + * + * This is largely borrowed from \Drupal\Core\Installer\Form\SiteSettingsForm. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * The form structure. + * + * @todo Private files directory not yet implemented, depends on + * https://www.drupal.org/node/2547125. + */ + public function buildCredentialForm(array $form, FormStateInterface $form_state) { + $form['#title'] = $this->t('Drupal Upgrade'); + + $drivers = $this->getDatabaseTypes(); + $drivers_keys = array_keys($drivers); + // @todo https://www.drupal.org/node/2678510 Because this is a multi-step + // form, the form is not rebuilt during submission. Ideally we would get + // the chosen driver from form input, if available, in order to use + // #limit_validation_errors in the same way + // \Drupal\Core\Installer\Form\SiteSettingsForm does. + $default_driver = current($drivers_keys); + + $default_options = []; + + $form['database'] = [ + '#type' => 'details', + '#title' => $this->t('Source database'), + '#description' => $this->t('Provide credentials for the database of the Drupal site you want to upgrade.'), + '#open' => TRUE, + ]; + + $form['database']['driver'] = [ + '#type' => 'radios', + '#title' => $this->t('Database type'), + '#required' => TRUE, + '#default_value' => $default_driver, + ]; + if (count($drivers) == 1) { + $form['database']['driver']['#disabled'] = TRUE; + } + + // Add driver-specific configuration options. + foreach ($drivers as $key => $driver) { + $form['database']['driver']['#options'][$key] = $driver->name(); + + $form['database']['settings'][$key] = $driver->getFormOptions($default_options); + // @todo https://www.drupal.org/node/2678510 Using + // #limit_validation_errors in the submit does not work so it is not + // possible to require the database and username for mysql and pgsql. + // This is because this is a multi-step form. + $form['database']['settings'][$key]['database']['#required'] = FALSE; + $form['database']['settings'][$key]['username']['#required'] = FALSE; + $form['database']['settings'][$key]['#prefix'] = '

' . $this->t('@driver_name settings', ['@driver_name' => $driver->name()]) . '

'; + $form['database']['settings'][$key]['#type'] = 'container'; + $form['database']['settings'][$key]['#tree'] = TRUE; + $form['database']['settings'][$key]['advanced_options']['#parents'] = [$key]; + $form['database']['settings'][$key]['#states'] = [ + 'visible' => [ + ':input[name=driver]' => ['value' => $key], + ], + ]; + + // Move the host fields out of advanced settings. + if (isset($form['database']['settings'][$key]['advanced_options']['host'])) { + $form['database']['settings'][$key]['host'] = $form['database']['settings'][$key]['advanced_options']['host']; + $form['database']['settings'][$key]['host']['#title'] = 'Database host'; + $form['database']['settings'][$key]['host']['#weight'] = -1; + unset($form['database']['settings'][$key]['database']['#default_value']); + unset($form['database']['settings'][$key]['advanced_options']['host']); + } + } + + $form['source'] = [ + '#type' => 'details', + '#title' => $this->t('Source files'), + '#open' => TRUE, + ]; + $form['source']['source_base_path'] = [ + '#type' => 'textfield', + '#title' => $this->t('Files directory'), + '#description' => $this->t('To import files from your current Drupal site, enter a local file directory containing your site (e.g. /var/www/docroot), or your site address (e.g. http://example.com).'), + ]; + + $form['actions'] = ['#type' => 'actions']; + $form['actions']['save'] = [ + '#type' => 'submit', + '#value' => $this->t('Review upgrade'), + '#button_type' => 'primary', + '#validate' => ['::validateCredentialForm'], + '#submit' => ['::submitCredentialForm'], + ]; + return $form; + } + + /** + * Validation handler for the credentials form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function validateCredentialForm(array &$form, FormStateInterface $form_state) { + // Skip if rollback was chosen. + if ($form_state->getValue('upgrade_option') == static::MIGRATE_UPGRADE_ROLLBACK) { + return; + } + + // 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. + if ($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; + } + } + else { + $database = []; + // Migration templates that had matching tags for the source Drupal + // version where converted to migration entities. Find one of those + // migrations to be able to look up the matching database credentials + // from state. + $query = $this->entityStorage->getQuery('OR'); + $ids = $query->execute(); + foreach ($ids as $id) { + /** @var \Drupal\migrate\Entity\MigrationInterface $migration */ + $migration = Migration::load($id); + $is_drupal_migration = FALSE; + foreach ($migration->get('migration_tags') as $migration_tag) { + if (substr($migration_tag, 0, 7) === 'Drupal ') { + $is_drupal_migration = TRUE; + break; + } + } + if ($is_drupal_migration) { + $source = $migration->get('source'); + if ($database = $this->state->get($source['database_state_key'])['database']) { + break; + } + } + } + } + + try { + // Get the template for migration. + $migration_template = $this->getMigrationTemplates($database, $form_state->getValue('source_base_path')); + + // Get a copy of all the relevant migrations so we run them in next step. + $migrations = $this->getMigrations($migration_template); + + // Get the system data from source database. + $system_data = $this->getSystemData($database); + + // Convert the migration object into array + // so that it can be stored in form storage. + $migration_array = []; + foreach ($migrations as $migration) { + $migration_array[] = $migration->toArray(); + } + + // Store the retrieved migration templates in form storage. + $form_state->set('migration_template', $migration_template); + + // Store the retrieved migration ids in form storage. + $form_state->set('migration', $migration_array); + + // Store the retrived system data in from storage. + $form_state->set('system_data', $system_data); + } + catch (\Exception $e) { + $error_message = [ + '#type' => 'inline_template', + '#template' => '{% trans %}Resolve the issue below to continue the upgrade.{% endtrans%}{{ errors }}', + '#context' => [ + 'errors' => [ + '#theme' => 'item_list', + '#items' => [$e->getMessage()], + ], + ], + ]; + + $form_state->setErrorByName($database['driver'] . '][0', $this->renderer->renderPlain($error_message)); + } + } + + /** + * Submission handler for the credentials form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function submitCredentialForm(array &$form, FormStateInterface $form_state) { + // Indicate the next step is confirmation. + $form_state->setValue('step', 'confirm'); + $form_state->setRebuild(); + } + + /** + * Confirmation form for rollbacks, missing migrations, etc. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * The form structure. + */ + public function buildConfirmForm(array $form, FormStateInterface $form_state) { + $form = parent::buildForm($form, $form_state); + $form['actions']['submit']['#submit'] = ['::submitConfirmForm']; + + if ($rollback = $form_state->getValue('upgrade_option') == static::MIGRATE_UPGRADE_ROLLBACK) { + $form_state->setStorage(['upgrade_option' => static::MIGRATE_UPGRADE_ROLLBACK]); + $form['rollback'] = [ + '#markup' => $this->t('All previously imported content, as well as configuration such as field definitions, will be removed.'), + ]; + $form['actions']['submit']['#value'] = $this->t('Perform rollback'); + } + else { + $form['actions']['submit']['#value'] = $this->t('Perform upgrade'); + + $table_data = []; + $system_data = []; + foreach ($form_state->get('migration') as $migration) { + $migration_id = $migration['id']; + // Fetch the system data at the first opportunity. + if (empty($system_data)) { + $system_data = $form_state->get('system_data'); + } + $template_id = $migration['template']; + $source_module = $this->moduleUpgradePaths[$template_id]['source_module']; + $destination_module = $this->moduleUpgradePaths[$template_id]['destination_module']; + $table_data[$source_module][$destination_module][$migration_id] = $migration['label']; + } + // Sort the table by source module names and within that destination + // module names. + ksort($table_data); + foreach ($table_data as $source_module => $destination_module_info) { + ksort($table_data[$source_module]); + } + $unmigrated_source_modules = array_diff_key($system_data['module'], $table_data); + + // Missing migrations. + $form['missing_module_list_title'] = [ + '#type' => 'item', + '#title' => $this->t('Missing upgrade paths'), + '#description' => $this->t('The following items will not be upgraded. For more information see Upgrading from Drupal 6 or 7 to Drupal 8.', array(':migrate' => 'https://www.drupal.org/upgrade/migrate')), + ]; + $form['missing_module_list'] = [ + '#type' => 'table', + '#header' => [ + $this->t('Source'), + $this->t('Destination'), + ], + ]; + $missing_count = 0; + ksort($unmigrated_source_modules); + foreach ($unmigrated_source_modules as $source_module => $module_data) { + if ($module_data['status']) { + $missing_count++; + $form['missing_module_list'][$source_module] = [ + 'source_module' => ['#plain_text' => $source_module], + 'destination_module' => ['#plain_text' => 'Missing'], + ]; + } + } + // Available migrations. + $form['available_module_list'] = [ + '#tree' => TRUE, + '#type' => 'details', + '#title' => $this->t('Available upgrade paths'), + ]; + + $form['available_module_list']['module_list'] = [ + '#type' => 'table', + '#header' => [ + $this->t('Source'), + $this->t('Destination'), + ], + ]; + + $available_count = 0; + foreach ($table_data as $source_module => $destination_module_info) { + $available_count++; + $destination_details = []; + foreach ($destination_module_info as $destination_module => $migration_ids) { + $destination_details[$destination_module] = [ + '#type' => 'item', + '#plain_text' => $destination_module, + ]; + } + $form['available_module_list']['module_list'][$source_module] = [ + 'source_module' => ['#plain_text' => $source_module], + 'destination_module' => $destination_details, + ]; + } + $form['counts'] = [ + '#type' => 'item', + '#title' => '', + '#weight' => -15, + ]; + } + + return $form; + } + + /** + * Submission handler for the confirmation form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function submitConfirmForm(array &$form, FormStateInterface $form_state) { + $storage = $form_state->getStorage(); + if (isset($storage['upgrade_option']) && $storage['upgrade_option'] == static::MIGRATE_UPGRADE_ROLLBACK) { + $query = $this->entityStorage->getQuery(); + $names = $query->execute(); + + // Order the migrations according to their dependencies. + /** @var \Drupal\migrate\Entity\MigrationInterface[] $migrations */ + $migrations = $this->entityStorage->loadMultiple($names); + // Assume we want all those tagged 'Drupal %'. + foreach ($migrations as $migration_id => $migration) { + $keep = FALSE; + $tags = $migration->get('migration_tags'); + foreach ($tags as $tag) { + if (strpos($tag, 'Drupal ') === 0) { + $keep = TRUE; + break; + } + } + if (!$keep) { + unset($migrations[$migration_id]); + } + } + // Roll back in reverse order. + $migrations = array_reverse($migrations); + + $batch = [ + 'title' => $this->t('Rolling back upgrade'), + 'progress_message' => '', + 'operations' => [ + [ + [MigrateUpgradeRunBatch::class, 'run'], + [array_keys($migrations), 'rollback'], + ], + ], + 'finished' => [ + MigrateUpgradeRunBatch::class, + 'finished', + ], + ]; + batch_set($batch); + $form_state->setRedirect('migrate_drupal_ui.upgrade'); + $this->state->delete('migrate_drupal_ui.performed'); + } + else { + $migration_template = $storage['migration_template']; + $migration_ids = $this->createMigrations($migration_template); + $batch = [ + 'title' => $this->t('Running upgrade'), + 'progress_message' => '', + 'operations' => [ + [ + [MigrateUpgradeRunBatch::class, 'run'], + [$migration_ids, 'import'], + ], + ], + 'finished' => [ + MigrateUpgradeRunBatch::class, + 'finished', + ], + ]; + batch_set($batch); + $form_state->setRedirect(''); + $this->state->set('migrate_drupal_ui.performed', REQUEST_TIME); + } + } + + /** + * 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.inc'; + return drupal_get_database_types(); + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Are you sure?'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('migrate_drupal_ui.upgrade'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('

Upgrade analysis report

'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Perform upgrade'); + } + +} 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..9e220f1 --- /dev/null +++ b/core/modules/migrate_drupal_ui/src/MigrateMessageCapture.php @@ -0,0 +1,48 @@ +messages[] = $message; + } + + /** + * Clears out any captured messages. + */ + public function clear() { + $this->messages = []; + } + + /** + * Returns any captured messages. + * + * @return array + * The captured messages. + */ + 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..669ec5e --- /dev/null +++ b/core/modules/migrate_drupal_ui/src/MigrateUpgradeRunBatch.php @@ -0,0 +1,371 @@ +addListener(MigrateEvents::POST_ROW_SAVE, [static::class, 'onPostRowSave']); + $event_dispatcher->addListener(MigrateEvents::MAP_SAVE, [static::class, 'onMapSave']); + $event_dispatcher->addListener(MigrateEvents::IDMAP_MESSAGE, [static::class, 'onIdMapMessage']); + } + else { + $event_dispatcher->addListener(MigrateEvents::POST_ROW_DELETE, [static::class, 'onPostRowDelete']); + $event_dispatcher->addListener(MigrateEvents::MAP_DELETE, [static::class, 'onMapDelete']); + } + static::$maxExecTime = ini_get('max_execution_time'); + if (static::$maxExecTime <= 0) { + static::$maxExecTime = 60; + } + // Set an arbitrary threshold of 3 seconds (e.g., if max_execution_time is + // 45 seconds, we will quit at 42 seconds so a slow item or cleanup + // overhead don't put us over 45). + static::$maxExecTime -= 3; + static::$listenersAdded = TRUE; + } + if (!isset($context['sandbox']['migration_ids'])) { + $context['sandbox']['max'] = count($initial_ids); + $context['sandbox']['current'] = 1; + // Total number processed for this migration. + $context['sandbox']['num_processed'] = 0; + // migration_ids will be the list of IDs remaining to run. + $context['sandbox']['migration_ids'] = $initial_ids; + $context['sandbox']['messages'] = []; + $context['results']['failures'] = 0; + $context['results']['successes'] = 0; + $context['results']['operation'] = $operation; + } + + // Number processed in this batch. + static::$numProcessed = 0; + + $migration_id = reset($context['sandbox']['migration_ids']); + /** @var \Drupal\migrate\Entity\Migration $migration */ + $migration = Migration::load($migration_id); + if ($migration) { + static::$messages = new MigrateMessageCapture(); + $executable = new MigrateExecutable($migration, static::$messages); + + $migration_name = $migration->label() ? $migration->label() : $migration_id; + + try { + if ($operation == 'import') { + $migration_status = $executable->import(); + } + else { + $migration_status = $executable->rollback(); + } + } + catch (\Exception $e) { + static::logger()->error($e->getMessage()); + $migration_status = MigrationInterface::RESULT_FAILED; + } + + switch ($migration_status) { + case MigrationInterface::RESULT_COMPLETED: + // Store the number processed in the sandbox. + $context['sandbox']['num_processed'] += static::$numProcessed; + if ($operation == 'import') { + $message = static::getTranslation()->formatPlural( + $context['sandbox']['num_processed'], 'Upgraded @migration (processed 1 item total)', 'Upgraded @migration (processed @num_processed items total)', + ['@migration' => $migration_name, '@num_processed' => $context['sandbox']['num_processed']]); + } + else { + $message = static::getTranslation()->formatPlural( + $context['sandbox']['num_processed'], 'Rolled back @migration (processed 1 item total)', 'Rolled back @migration (processed @num_processed items total)', + ['@migration' => $migration_name, '@num_processed' => $context['sandbox']['num_processed']]); + $migration->delete(); + } + $context['sandbox']['messages'][] = $message; + static::logger()->notice($message); + $context['sandbox']['num_processed'] = 0; + $context['results']['successes']++; + break; + + case MigrationInterface::RESULT_INCOMPLETE: + $context['sandbox']['messages'][] = static::getTranslation()->formatPlural( + static::$numProcessed, 'Continuing with @migration (processed 1 item)', 'Continuing with @migration (processed @num_processed items)', + ['@migration' => $migration_name, '@num_processed' => static::$numProcessed]); + $context['sandbox']['num_processed'] += static::$numProcessed; + break; + + case MigrationInterface::RESULT_STOPPED: + $context['sandbox']['messages'][] = t('Operation stopped by request'); + break; + + case MigrationInterface::RESULT_FAILED: + $context['sandbox']['messages'][] = t('Operation on @migration failed', ['@migration' => $migration_name]); + $context['results']['failures']++; + static::logger()->error('Operation on @migration failed', ['@migration' => $migration_name]); + break; + + case MigrationInterface::RESULT_SKIPPED: + $context['sandbox']['messages'][] = t('Operation on @migration skipped due to unfulfilled dependencies', ['@migration' => $migration_name]); + static::logger()->error('Operation on @migration skipped due to unfulfilled dependencies', ['@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']); + $context['sandbox']['current']++; + } + + // Add and log any captured messages. + foreach (static::$messages->getMessages() as $message) { + $context['sandbox']['messages'][] = $message; + static::logger()->error($message); + } + + // Only display the last MESSAGE_LENGTH messages, in reverse order. + $message_count = count($context['sandbox']['messages']); + $context['message'] = ''; + for ($index = max(0, $message_count - self::MESSAGE_LENGTH); $index < $message_count; $index++) { + $context['message'] = $context['sandbox']['messages'][$index] . "
\n" . $context['message']; + } + if ($message_count > self::MESSAGE_LENGTH) { + // 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; + if ($operation == 'import') { + $context['message'] = t('Currently upgrading @migration (@current of @max total tasks)', [ + '@migration' => $migration_name, + '@current' => $context['sandbox']['current'], + '@max' => $context['sandbox']['max'], + ]) . "
\n" . $context['message']; + } + else { + $context['message'] = t('Currently rolling back @migration (@current of @max total tasks)', [ + '@migration' => $migration_name, + '@current' => $context['sandbox']['current'], + '@max' => $context['sandbox']['max'], + ]) . "
\n" . $context['message']; + } + } + } + else { + array_shift($context['sandbox']['migration_ids']); + $context['sandbox']['current']++; + } + + $context['finished'] = 1 - count($context['sandbox']['migration_ids']) / $context['sandbox']['max']; + } + + /** + * Returns 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'); + } + + /** + * Wraps the translation manager. + * + * @return \Drupal\Core\StringTranslation\TranslationManager + * The string translation manager. + */ + protected static function getTranslation() { + return \Drupal::translation(); + } + + /** + * Implements the Batch API finished method. + */ + public static function finished($success, $results, $operations, $elapsed) { + static::displayResults($results); + } + + /** + * Displays counts of success/failures on the migration upgrade complete page. + * + * @param array $results + * An array of result data built during the batch. + */ + protected static function displayResults($results) { + $successes = $results['successes']; + $failures = $results['failures']; + + // If we had any successes log that for the user. + if ($successes > 0) { + if ($results['operation'] == 'import') { + drupal_set_message(static::getTranslation()->formatPlural($successes, 'Completed 1 upgrade task successfully', 'Completed @count upgrade tasks successfully')); + } + else { + drupal_set_message(static::getTranslation()->formatPlural($successes, 'Completed 1 rollback task successfully', 'Completed @count rollback tasks successfully')); + } + } + + // If we had failures, log them and show the migration failed. + if ($failures > 0) { + if ($results['operation'] == 'import') { + drupal_set_message(static::getTranslation()->formatPlural($failures, '1 upgrade failed', '@count upgrades failed')); + drupal_set_message(t('Upgrade process not completed'), 'error'); + } + else { + drupal_set_message(static::getTranslation()->formatPlural($failures, '1 rollback failed', '@count rollbacks failed')); + drupal_set_message(t('Rollback process not completed'), 'error'); + } + } + else { + if ($results['operation'] == 'import') { + // 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!')); + } + else { + drupal_set_message(t('Rollback of the upgrade is complete - you may now start the upgrade process from scratch.')); + } + } + + if (\Drupal::moduleHandler()->moduleExists('dblog')) { + $url = Url::fromRoute('migrate_drupal_ui.log'); + drupal_set_message(Link::fromTextAndUrl(t('Review the detailed upgrade log'), $url), $failures ? 'error' : 'status'); + } + } + + /** + * Reacts to item import. + * + * @param \Drupal\migrate\Event\MigratePostRowSaveEvent $event + * The post-save event. + */ + public static function onPostRowSave(MigratePostRowSaveEvent $event) { + // We want to interrupt this batch and start a fresh one. + if ((time() - REQUEST_TIME) > static::$maxExecTime) { + $event->getMigration()->interruptMigration(MigrationInterface::RESULT_INCOMPLETE); + } + } + + /** + * Reacts to item deletion. + * + * @param \Drupal\migrate\Event\MigrateRowDeleteEvent $event + * The post-save event. + */ + public static function onPostRowDelete(MigrateRowDeleteEvent $event) { + // We want to interrupt this batch and start a fresh one. + if ((time() - REQUEST_TIME) > static::$maxExecTime) { + $event->getMigration()->interruptMigration(MigrationInterface::RESULT_INCOMPLETE); + } + } + + /** + * Counts up any map save events. + * + * @param \Drupal\migrate\Event\MigrateMapSaveEvent $event + * The map event. + */ + public static function onMapSave(MigrateMapSaveEvent $event) { + static::$numProcessed++; + } + + /** + * Counts up any map delete events. + * + * @param \Drupal\migrate\Event\MigrateMapDeleteEvent $event + * The map event. + */ + public static function onMapDelete(MigrateMapDeleteEvent $event) { + static::$numProcessed++; + } + + /** + * Displays any messages being logged to the ID map. + * + * @param \Drupal\migrate\Event\MigrateIdMapMessageEvent $event + * The message event. + */ + public static function onIdMapMessage(MigrateIdMapMessageEvent $event) { + if ($event->getLevel() == MigrationInterface::MESSAGE_NOTICE || $event->getLevel() == MigrationInterface::MESSAGE_INFORMATIONAL) { + $type = 'status'; + } + else { + $type = 'error'; + } + $source_id_string = implode(',', $event->getSourceIdValues()); + $message = t('Source ID @source_id: @message', ['@source_id' => $source_id_string, '@message' => $event->getMessage()]); + static::$messages->display($message, $type); + } + +} diff --git a/core/modules/migrate_drupal_ui/src/Tests/MigrateUpgradeTestBase.php b/core/modules/migrate_drupal_ui/src/Tests/MigrateUpgradeTestBase.php new file mode 100644 index 0000000..a4911aa --- /dev/null +++ b/core/modules/migrate_drupal_ui/src/Tests/MigrateUpgradeTestBase.php @@ -0,0 +1,164 @@ +createMigrationConnection(); + $this->sourceDatabase = Database::getConnection('default', 'migrate_drupal_ui'); + + // Create and log in as user 1. Migrations in the UI can only be performed + // as user 1 once https://www.drupal.org/node/2675066 lands. + $this->drupalLogin($this->rootUser); + } + + /** + * Loads a database fixture into the source database connection. + * + * @param string $path + * Path to the dump file. + */ + protected function loadFixture($path) { + $default_db = Database::getConnection()->getKey(); + Database::setActiveConnection($this->sourceDatabase->getKey()); + + if (substr($path, -3) == '.gz') { + $path = 'compress.zlib://' . $path; + } + require $path; + + Database::setActiveConnection($default_db); + } + + /** + * Changes the database connection to the prefixed one. + * + * @todo Remove when we don't use global. https://www.drupal.org/node/2552791 + */ + protected function createMigrationConnection() { + $connection_info = Database::getConnectionInfo('default')['default']; + if ($connection_info['driver'] === 'sqlite') { + // Create database file in the test site's public file directory so that + // \Drupal\simpletest\TestBase::restoreEnvironment() will delete this once + // the test is complete. + $file = $this->publicFilesDirectory . '/' . $this->testId . '-migrate.db.sqlite'; + touch($file); + $connection_info['database'] = $file; + $connection_info['prefix'] = ''; + } + else { + $prefix = is_array($connection_info['prefix']) ? $connection_info['prefix']['default'] : $connection_info['prefix']; + // Simpletest uses fixed length prefixes. Create a new prefix for the + // source database. Adding to the end of the prefix ensures that + // \Drupal\simpletest\TestBase::restoreEnvironment() will remove the + // additional tables. + $connection_info['prefix'] = $prefix . '0'; + } + + Database::addConnectionInfo('migrate_drupal_ui', 'default', $connection_info); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() { + Database::removeConnection('migrate_drupal_ui'); + parent::tearDown(); + } + + /** + * Executes all steps of migrations upgrade. + */ + protected function testMigrateUpgrade() { + $connection_options = $this->sourceDatabase->getConnectionOptions(); + $this->drupalGet('/upgrade'); + $this->assertText('Upgrade a Drupal site by importing it into a clean and empty new install of Drupal 8. You will lose any existing configuration once you import your site into it. See the upgrading handbook for more detailed information.'); + + $this->drupalPostForm(NULL, [], t('Continue')); + $this->assertText('Provide credentials for the database of the Drupal site you want to upgrade.'); + $this->assertFieldByName('mysql[host]'); + + $driver = $connection_options['driver']; + $connection_options['prefix'] = $connection_options['prefix']['default']; + + // Use the driver connection form to get the correct options out of the + // database settings. This supports all of the databases we test against. + $drivers = drupal_get_database_types(); + $form = $drivers[$driver]->getFormOptions($connection_options); + $connection_options = array_intersect_key($connection_options, $form + $form['advanced_options']); + $edits = $this->translatePostValues([ + 'driver' => $driver, + $driver => $connection_options, + 'source_base_path' => $this->getSourceBasePath(), + ]); + + $this->drupalPostForm(NULL, $edits, t('Review upgrade')); + $this->assertResponse(200); + $this->assertText('Are you sure?'); + $this->drupalPostForm(NULL, [], t('Perform upgrade')); + $this->assertText(t('Congratulations, you upgraded Drupal!')); + + // Have to reset all the statics after migration to ensure entities are + // loadable. + $this->resetAll(); + + $expected_counts = $this->getEntityCounts(); + foreach (array_keys(\Drupal::entityTypeManager()->getDefinitions()) as $entity_type) { + $real_count = count(\Drupal::entityTypeManager()->getStorage($entity_type)->loadMultiple()); + $expected_count = isset($expected_counts[$entity_type]) ? $expected_counts[$entity_type] : 0; + $this->assertEqual($expected_count, $real_count, "Found $real_count $entity_type entities, expected $expected_count."); + } + } + + /** + * Gets the source base path for the concrete test. + * + * @return string + * The source base path. + */ + abstract protected function getSourceBasePath(); + + /** + * Gets the expected number of entities per entity type after migration. + * + * @return int[] + * An array of expected counts keyed by entity type ID. + */ + abstract protected function getEntityCounts(); + +} diff --git a/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php b/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php new file mode 100644 index 0000000..6935470 --- /dev/null +++ b/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php @@ -0,0 +1,78 @@ +loadFixture(drupal_get_path('module', 'migrate_drupal') . '/tests/fixtures/drupal6.php'); + } + + /** + * {@inheritdoc} + */ + protected function getSourceBasePath() { + return __DIR__ . '/files'; + } + + /** + * {@inheritdoc} + */ + protected function getEntityCounts() { + return [ + 'block' => 30, + 'block_content' => 2, + 'block_content_type' => 1, + 'comment' => 3, + 'comment_type' => 2, + 'contact_form' => 5, + 'editor' => 2, + 'field_config' => 61, + 'field_storage_config' => 42, + 'file' => 4, + 'filter_format' => 8, + 'image_style' => 5, + 'migration' => 105, + 'node' => 9, + 'node_type' => 11, + 'rdf_mapping' => 5, + 'search_page' => 2, + 'shortcut' => 2, + 'shortcut_set' => 1, + 'action' => 22, + 'menu' => 8, + 'taxonomy_term' => 6, + 'taxonomy_vocabulary' => 6, + 'tour' => 1, + 'user' => 7, + 'user_role' => 6, + 'menu_link_content' => 4, + 'view' => 12, + 'date_format' => 11, + 'entity_form_display' => 15, + 'entity_form_mode' => 1, + 'entity_view_display' => 32, + 'entity_view_mode' => 12, + 'base_field_override' => 33, + ]; + } + +} diff --git a/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php b/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php new file mode 100644 index 0000000..a546554 --- /dev/null +++ b/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php @@ -0,0 +1,78 @@ +loadFixture(drupal_get_path('module', 'migrate_drupal') . '/tests/fixtures/drupal7.php'); + } + + /** + * {@inheritdoc} + */ + protected function getSourceBasePath() { + return __DIR__ . '/files'; + } + + /** + * {@inheritdoc} + */ + protected function getEntityCounts() { + return [ + 'block' => 25, + 'block_content' => 1, + 'block_content_type' => 1, + 'comment' => 1, + 'comment_type' => 7, + 'contact_form' => 3, + 'editor' => 2, + 'field_config' => 40, + 'field_storage_config' => 30, + 'file' => 0, + 'filter_format' => 7, + 'image_style' => 6, + 'migration' => 59, + 'node' => 2, + 'node_type' => 6, + 'rdf_mapping' => 5, + 'search_page' => 2, + 'shortcut' => 6, + 'shortcut_set' => 2, + 'action' => 18, + 'menu' => 10, + 'taxonomy_term' => 18, + 'taxonomy_vocabulary' => 3, + 'tour' => 1, + 'user' => 3, + 'user_role' => 4, + 'menu_link_content' => 9, + 'view' => 12, + 'date_format' => 11, + 'entity_form_display' => 15, + 'entity_form_mode' => 1, + 'entity_view_display' => 22, + 'entity_view_mode' => 10, + 'base_field_override' => 7, + ]; + } + +}