diff --git a/core/modules/aggregator/migration_templates/d6_aggregator_feed.yml b/core/modules/aggregator/migration_templates/d6_aggregator_feed.yml index cad1553..8689d5b 100644 --- a/core/modules/aggregator/migration_templates/d6_aggregator_feed.yml +++ b/core/modules/aggregator/migration_templates/d6_aggregator_feed.yml @@ -1,5 +1,6 @@ id: d6_aggregator_feed label: Aggregator feeds +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/aggregator/migration_templates/d6_aggregator_item.yml b/core/modules/aggregator/migration_templates/d6_aggregator_item.yml index e14dbd6..7c991eb 100644 --- a/core/modules/aggregator/migration_templates/d6_aggregator_item.yml +++ b/core/modules/aggregator/migration_templates/d6_aggregator_item.yml @@ -1,5 +1,6 @@ id: d6_aggregator_item label: Aggregator items +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/aggregator/migration_templates/d7_aggregator_feed.yml b/core/modules/aggregator/migration_templates/d7_aggregator_feed.yml index 5dbeb25..48eb29d 100644 --- a/core/modules/aggregator/migration_templates/d7_aggregator_feed.yml +++ b/core/modules/aggregator/migration_templates/d7_aggregator_feed.yml @@ -1,5 +1,6 @@ id: d7_aggregator_feed label: Aggregator feeds +audit: true migration_tags: - Drupal 7 source: diff --git a/core/modules/aggregator/migration_templates/d7_aggregator_item.yml b/core/modules/aggregator/migration_templates/d7_aggregator_item.yml index 054ba43..342c5c8 100644 --- a/core/modules/aggregator/migration_templates/d7_aggregator_item.yml +++ b/core/modules/aggregator/migration_templates/d7_aggregator_item.yml @@ -1,5 +1,6 @@ id: d7_aggregator_item label: Aggregator items +audit: true migration_tags: - Drupal 7 source: diff --git a/core/modules/block_content/migration_templates/d6_custom_block.yml b/core/modules/block_content/migration_templates/d6_custom_block.yml index 55fbcb5..071e4de 100644 --- a/core/modules/block_content/migration_templates/d6_custom_block.yml +++ b/core/modules/block_content/migration_templates/d6_custom_block.yml @@ -1,5 +1,6 @@ id: d6_custom_block label: Custom blocks +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/block_content/migration_templates/d7_custom_block.yml b/core/modules/block_content/migration_templates/d7_custom_block.yml index ca06cf0..1a9ea19 100644 --- a/core/modules/block_content/migration_templates/d7_custom_block.yml +++ b/core/modules/block_content/migration_templates/d7_custom_block.yml @@ -1,5 +1,6 @@ id: d7_custom_block label: Custom blocks +audit: true migration_tags: - Drupal 7 source: diff --git a/core/modules/comment/migration_templates/d6_comment.yml b/core/modules/comment/migration_templates/d6_comment.yml index 161820e..afab0cb 100644 --- a/core/modules/comment/migration_templates/d6_comment.yml +++ b/core/modules/comment/migration_templates/d6_comment.yml @@ -1,5 +1,6 @@ id: d6_comment label: Comments +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/comment/migration_templates/d7_comment.yml b/core/modules/comment/migration_templates/d7_comment.yml index dff4b64..0837df9 100644 --- a/core/modules/comment/migration_templates/d7_comment.yml +++ b/core/modules/comment/migration_templates/d7_comment.yml @@ -1,5 +1,6 @@ id: d7_comment label: Comments +audit: true migration_tags: - Drupal 7 source: diff --git a/core/modules/file/migration_templates/d6_file.yml b/core/modules/file/migration_templates/d6_file.yml index 6544d7d..5b8b672 100644 --- a/core/modules/file/migration_templates/d6_file.yml +++ b/core/modules/file/migration_templates/d6_file.yml @@ -2,6 +2,7 @@ # migration as an optional dependency. id: d6_file label: Public files +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/file/migration_templates/d7_file.yml b/core/modules/file/migration_templates/d7_file.yml index b63f13e..7e05a28 100644 --- a/core/modules/file/migration_templates/d7_file.yml +++ b/core/modules/file/migration_templates/d7_file.yml @@ -2,6 +2,7 @@ # migration as an optional dependency. id: d7_file label: Public files +audit: true migration_tags: - Drupal 7 source: diff --git a/core/modules/file/migration_templates/d7_file_private.yml b/core/modules/file/migration_templates/d7_file_private.yml index 197c701..51de533 100644 --- a/core/modules/file/migration_templates/d7_file_private.yml +++ b/core/modules/file/migration_templates/d7_file_private.yml @@ -1,5 +1,6 @@ id: d7_file_private label: Private files +audit: true migration_tags: - Drupal 7 source: diff --git a/core/modules/menu_link_content/migration_templates/d6_menu_links.yml b/core/modules/menu_link_content/migration_templates/d6_menu_links.yml index 2c8ad4a..e05efee 100644 --- a/core/modules/menu_link_content/migration_templates/d6_menu_links.yml +++ b/core/modules/menu_link_content/migration_templates/d6_menu_links.yml @@ -1,5 +1,6 @@ id: d6_menu_links label: Menu links +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/menu_link_content/migration_templates/d7_menu_links.yml b/core/modules/menu_link_content/migration_templates/d7_menu_links.yml index 200a792..81d4eb8 100644 --- a/core/modules/menu_link_content/migration_templates/d7_menu_links.yml +++ b/core/modules/menu_link_content/migration_templates/d7_menu_links.yml @@ -1,5 +1,6 @@ id: d7_menu_links label: Menu links +audit: true migration_tags: - Drupal 7 source: diff --git a/core/modules/migrate/src/Audit/AuditException.php b/core/modules/migrate/src/Audit/AuditException.php new file mode 100644 index 0000000..3d122e3 --- /dev/null +++ b/core/modules/migrate/src/Audit/AuditException.php @@ -0,0 +1,27 @@ +id(), $message); + parent::__construct($message, 0, $previous); + } + +} diff --git a/core/modules/migrate/src/Audit/AuditorInterface.php b/core/modules/migrate/src/Audit/AuditorInterface.php new file mode 100644 index 0000000..331a807 --- /dev/null +++ b/core/modules/migrate/src/Audit/AuditorInterface.php @@ -0,0 +1,44 @@ +getPluginDefinition(); + + if (empty($plugin_definition['audit'])) { + return []; + } + + $interface = MaximumValueInterface::class; + + $destination = $migration->getDestinationPlugin(); + if (!$destination instanceof MaximumValueInterface) { + throw new AuditException($migration, "Destination does not implement $interface"); + } + + $id_map = $migration->getIdMap(); + if (!$id_map instanceof MaximumValueInterface) { + throw new AuditException($migration, "ID map does not implement $interface"); + } + + if ($destination->max() > $id_map->max()) { + return static::conflict($migration); + } + return []; + } + + /** + * {@inheritdoc} + */ + public function auditMultiple(array $migrations) { + $conflicts = []; + + foreach ($migrations as $migration) { + $conflicts += $this->audit($migration); + } + ksort($conflicts); + return $conflicts; + } + + /** + * Builds a conflict array. + * + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The migration that has the conflict. + * + * @return array + * The conflict. See \Drupal\migrate\Audit\AuditorInterface::audit() for + * information on the format of the return value. + */ + public static function conflict(MigrationInterface $migration) { + $base_id = $migration->getBaseId(); + + $conflict = [ + $base_id => $base_id, + ]; + + $label = $migration->label(); + if (is_string($label)) { + $conflict[$base_id] = $label; + } + elseif ($label instanceof TranslatableMarkup) { + $arguments = $label->getArguments(); + $conflict[$base_id] = isset($arguments['@label']) ? $arguments['@label'] : $label->render(); + } + return $conflict; + } + +} diff --git a/core/modules/migrate/src/Audit/MaximumValueInterface.php b/core/modules/migrate/src/Audit/MaximumValueInterface.php new file mode 100644 index 0000000..4d6eb16 --- /dev/null +++ b/core/modules/migrate/src/Audit/MaximumValueInterface.php @@ -0,0 +1,26 @@ +configuration['translations']); } @@ -294,4 +292,15 @@ protected function getDefinitionFromEntity($key) { ] + $field_definition->getSettings(); } + /** + * {@inheritdoc} + */ + public function max() { + $query = $this->storage->getQuery() + ->sort($this->getKey('id'), 'DESC') + ->range(0, 1); + $result = $query->execute(); + return (int) reset($result); + } + } diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php index b0db476..336e765 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php @@ -78,4 +78,15 @@ public function getIds() { throw new MigrateException('This entity type does not support revisions.'); } + /** + * {@inheritdoc} + */ + public function max() { + $query = $this->storage->getQuery() + ->sort($this->getKey('revision'), 'DESC') + ->range(0, 1); + $result = $query->execute(); + return (int) reset($result); + } + } diff --git a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php index 6bdd51e..98485e8 100644 --- a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php +++ b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php @@ -7,6 +7,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\migrate\MigrateMessage; +use Drupal\migrate\Audit\MaximumValueInterface; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Event\MigrateIdMapMessageEvent; use Drupal\migrate\MigrateException; @@ -27,7 +28,7 @@ * * @PluginID("sql") */ -class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryPluginInterface { +class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryPluginInterface, MaximumValueInterface { /** * Column name of hashed source id values. @@ -152,6 +153,8 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP * The configuration for the plugin. * @param \Drupal\migrate\Plugin\MigrationInterface $migration * The migration to do. + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher + * The event dispatcher. */ public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EventDispatcherInterface $event_dispatcher) { parent::__construct($configuration, $plugin_id, $plugin_definition); @@ -169,7 +172,8 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_id, $plugin_definition, $migration, - $container->get('event_dispatcher') + $container->get('event_dispatcher'), + $container->get('plugin.manager.migration') ); } @@ -925,4 +929,63 @@ public function valid() { return $this->currentRow !== FALSE; } + /** + * {@inheritdoc} + */ + public function max() { + $destination_ids = array_filter( + $this->migration->getDestinationPlugin()->getIds(), + function (array $id) { + return $id['type'] == 'integer'; + } + ); + if (empty($destination_ids)) { + throw new \LogicException('Cannot determine the highest migrated ID without an integer ID column'); + } + $field_name = key($destination_ids); + + $sql_field = $this->destinationIdFields()[$field_name]; + $migration_id = $this->migration->id(); + + // List of mapping tables to look in for the highest ID. + $map_tables = [ + $migration_id => $this->mapTableName(), + ]; + + // If there's a bundle, it means we have a derived migration and we need to + // find all the mapping tables from the related derived migrations. + if ($base_id = substr($migration_id, 0, strpos($migration_id, static::DERIVATIVE_SEPARATOR))) { + // @TODO Inject the plugin manager as a dependency. + /** @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_manager */ + $migration_manager = \Drupal::service('plugin.manager.migration'); + + $migrations = $migration_manager->getDefinitions(); + foreach ($migrations as $migration_id => $migration) { + if ($migration['id'] == $base_id) { + // Get this derived migration's mapping table and add it to the list + // of mapping tables to look in for the highest ID. + $stub = $migration_manager->createInstance($migration_id); + $map_tables[$migration_id] = $stub->getIdMap()->mapTableName(); + } + } + } + + // Get the highest id from the list of map tables. + $ids = [0]; + foreach ($map_tables as $map_table) { + if (!$this->getDatabase()->schema()->tableExists($map_table)) { + break; + } + + $query = $this->getDatabase()->select($map_table, 'map') + ->fields('map', [$sql_field]) + ->orderBy($sql_field, 'DESC') + ->range(0, 1); + $ids[] = $query->execute()->fetchField(); + } + + // Return the highest of the highest IDs. + return max($ids); + } + } diff --git a/core/modules/migrate/tests/src/Kernel/MigrateTestBase.php b/core/modules/migrate/tests/src/Kernel/MigrateTestBase.php index 51e61d4..7f309b5 100644 --- a/core/modules/migrate/tests/src/Kernel/MigrateTestBase.php +++ b/core/modules/migrate/tests/src/Kernel/MigrateTestBase.php @@ -7,7 +7,6 @@ use Drupal\migrate\MigrateExecutable; use Drupal\migrate\MigrateMessageInterface; use Drupal\migrate\Plugin\MigrateIdMapInterface; -use Drupal\migrate\Plugin\Migration; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Row; @@ -47,6 +46,9 @@ */ protected $sourceDatabase; + /** + * {@inheritdoc} + */ public static $modules = ['migrate']; /** diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6AuditIdsTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6AuditIdsTest.php new file mode 100644 index 0000000..13128fb --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6AuditIdsTest.php @@ -0,0 +1,195 @@ +coreModuleListDataProvider()); + parent::setUp(); + + // Install required schemas. + $this->installEntitySchema('aggregator_item'); + $this->installEntitySchema('aggregator_feed'); + $this->installEntitySchema('block_content'); + $this->installEntitySchema('comment'); + $this->installEntitySchema('file'); + $this->installEntitySchema('menu_link_content'); + $this->installEntitySchema('node'); + $this->installEntitySchema('taxonomy_term'); + $this->installSchema('book', ['book']); + $this->installSchema('dblog', ['watchdog']); + $this->installSchema('forum', ['forum_index']); + $this->installSchema('search', ['search_dataset']); + $this->installSchema('system', ['sequences']); + $this->installSchema('tracker', ['tracker_node', 'tracker_user']); + } + + /** + * Tests multiple migrations to the same destination with no ID conflicts. + */ + public function testMultipleMigrationWithoutIdConflicts() { + // Create a node of type page. + Node::create(['type' => 'page', 'title' => 'foo'])->save(); + + // Insert data in the d6_node:page migration mappping table to simulate a + // previously migrated node. + $table_name = $this->getMigration('d6_node:page')->getIdMap()->mapTableName(); + $this->container->get('database')->insert($table_name) + ->fields([ + 'source_ids_hash' => 1, + 'sourceid1' => 1, + 'destid1' => 1, + ]) + ->execute(); + + // Audit the IDs of the d6_node migrations for the page & article node type. + // There should be no conflicts since the highest destination ID should be + // equal to the highest migrated ID, as found in the aggregated mapping + // tables of the two node migrations. + $migrations = [ + $this->getMigration('d6_node:page'), + $this->getMigration('d6_node:article'), + ]; + $this->assertEmpty((new IdAuditor())->auditMultiple($migrations)); + } + + /** + * Tests all migrations with no ID conflicts. + */ + public function testAllMigrationsWithNoIdConflicts() { + $migrations = $this->container + ->get('plugin.manager.migration') + ->createInstancesByTag('Drupal 6'); + + // Audit all Drupal 6 migrations that support it. There should be no + // conflicts since no content has been created. + $this->assertEmpty((new IdAuditor())->auditMultiple($migrations)); + } + + /** + * Tests all migrations with ID conflicts. + */ + public function testAllMigrationsWithIdConflicts() { + // Get all Drupal 6 migrations. + $migrations = $this->container + ->get('plugin.manager.migration') + ->createInstancesByTag('Drupal 6'); + + // Create content. + $this->createContent(); + + // Audit the IDs of all migrations. There should be conflicts since content + // has been created. + $conflicts = (new IdAuditor())->auditMultiple($migrations); + + $expected = [ + 'd6_aggregator_feed', + 'd6_aggregator_item', + 'd6_comment', + 'd6_custom_block', + 'd6_file', + 'd6_menu_links', + 'd6_node', + 'd6_node_revision', + 'd6_taxonomy_term', + 'd6_term_node_revision', + 'd6_user', + ]; + + $this->assertSame($expected, array_keys($conflicts)); + } + + protected function createContent() { + // Create an aggregator feed. + $feed = Feed::create([ + 'title' => 'feed', + 'url' => 'http://www.example.com', + ]); + $feed->save(); + + // Create an aggregator feed item. + $item = Item::create([ + 'title' => 'feed item', + 'fid' => $feed->id(), + 'link' => 'http://www.example.com', + ]); + $item->save(); + + // Create a block content. + $block = BlockContent::create([ + 'info' => 'block', + 'type' => 'block', + ]); + $block->save(); + + // Create a node. + $node = Node::create([ + 'type' => 'page', + 'title' => 'page', + ]); + $node->save(); + + // Create a comment. + $comment = Comment::create([ + 'comment_type' => 'comment', + 'field_name' => 'comment', + 'entity_type' => 'node', + 'entity_id' => $node->id(), + ]); + $comment->save(); + + // Create a file. + $file = File::create([ + 'uri' => 'public://example.txt', + ]); + $file->save(); + + // Create a menu link. + $menu_link = MenuLinkContent::create([ + 'title' => 'menu link', + 'link' => ['uri' => 'http://www.example.com'], + 'menu_name' => 'tools', + ]); + $menu_link->save(); + + // Create a taxonomy term. + $term = Term::create([ + 'name' => 'term', + 'vid' => 'term', + ]); + $term->save(); + + // Create a user. + $user = User::create([ + 'uid' => 2, + 'name' => 'user', + 'mail' => 'user@example.com', + ]); + $user->save(); + } + +} diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php new file mode 100644 index 0000000..b8d4f40 --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php @@ -0,0 +1,194 @@ +coreModuleListDataProvider()); + parent::setUp(); + + // Install required schemas. + $this->installEntitySchema('aggregator_item'); + $this->installEntitySchema('aggregator_feed'); + $this->installEntitySchema('block_content'); + $this->installEntitySchema('comment'); + $this->installEntitySchema('file'); + $this->installEntitySchema('menu_link_content'); + $this->installEntitySchema('node'); + $this->installEntitySchema('taxonomy_term'); + $this->installSchema('book', ['book']); + $this->installSchema('dblog', ['watchdog']); + $this->installSchema('forum', ['forum_index']); + $this->installSchema('search', ['search_dataset']); + $this->installSchema('system', ['sequences']); + $this->installSchema('tracker', ['tracker_node', 'tracker_user']); + } + + /** + * Tests multiple migrations to the same destination with no ID conflicts. + */ + public function testMultipleMigrationWithoutIdConflicts() { + // Create a node of type page. + Node::create(['type' => 'page', 'title' => 'foo'])->save(); + + // Insert data in the d7_node:page migration mappping table to simulate a + // previously migrated node. + $table_name = $this->getMigration('d7_node:page')->getIdMap()->mapTableName(); + $this->container->get('database')->insert($table_name) + ->fields([ + 'source_ids_hash' => 1, + 'sourceid1' => 1, + 'destid1' => 1, + ]) + ->execute(); + + // Audit the IDs of the d7_node migrations for the page & article node type. + // There should be no conflicts since the highest destination ID should be + // equal to the highest migrated ID, as found in the aggregated mapping + // tables of the two node migrations. + $migrations = [ + $this->getMigration('d7_node:page'), + $this->getMigration('d7_node:article'), + ]; + $this->assertEmpty((new IdAuditor())->auditMultiple($migrations)); + } + + /** + * Tests all migrations with no ID conflicts. + */ + public function testAllMigrationsWithNoIdConflicts() { + $migrations = $this->container + ->get('plugin.manager.migration') + ->createInstancesByTag('Drupal 7'); + + // Audit the IDs of all Drupal 7 migrations. There should be no conflicts + // since no content has been created. + $this->assertEmpty((new IdAuditor())->auditMultiple($migrations)); + } + + /** + * Tests all migrations with ID conflicts. + */ + public function testAllMigrationsWithIdConflicts() { + $migrations = $this->container + ->get('plugin.manager.migration') + ->createInstancesByTag('Drupal 7'); + + // Create content. + $this->createContent(); + + // Audit the IDs of all Drupal 7 migrations. There should be conflicts since + // content has been created. + $conflicts = (new IdAuditor())->auditMultiple($migrations); + + $expected = [ + 'd7_aggregator_feed', + 'd7_aggregator_item', + 'd7_comment', + 'd7_custom_block', + 'd7_file', + 'd7_file_private', + 'd7_menu_links', + 'd7_node', + 'd7_node_revision', + 'd7_taxonomy_term', + 'd7_user', + ]; + + $this->assertSame($expected, array_keys($conflicts)); + } + + protected function createContent() { + // Create an aggregator feed. + $feed = Feed::create([ + 'title' => 'feed', + 'url' => 'http://www.example.com', + ]); + $feed->save(); + + // Create an aggregator feed item. + $item = Item::create([ + 'title' => 'feed item', + 'fid' => $feed->id(), + 'link' => 'http://www.example.com', + ]); + $item->save(); + + // Create a block content. + $block = BlockContent::create([ + 'info' => 'block', + 'type' => 'block', + ]); + $block->save(); + + // Create a node. + $node = Node::create([ + 'type' => 'page', + 'title' => 'page', + ]); + $node->save(); + + // Create a comment. + $comment = Comment::create([ + 'comment_type' => 'comment', + 'field_name' => 'comment', + 'entity_type' => 'node', + 'entity_id' => $node->id(), + ]); + $comment->save(); + + // Create a file. + $file = File::create([ + 'uri' => 'public://example.txt', + ]); + $file->save(); + + // Create a menu link. + $menu_link = MenuLinkContent::create([ + 'title' => 'menu link', + 'link' => ['uri' => 'http://www.example.com'], + 'menu_name' => 'tools', + ]); + $menu_link->save(); + + // Create a taxonomy term. + $term = Term::create([ + 'name' => 'term', + 'vid' => 'term', + ]); + $term->save(); + + // Create a user. + $user = User::create([ + 'uid' => 2, + 'name' => 'user', + 'mail' => 'user@example.com', + ]); + $user->save(); + } + +} diff --git a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php index d747e0a..784281f 100644 --- a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php +++ b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php @@ -8,6 +8,8 @@ use Drupal\Core\Render\RendererInterface; use Drupal\Core\State\StateInterface; use Drupal\Core\Url; +use Drupal\migrate\Audit\IdAuditor; +use Drupal\migrate\Plugin\migrate\destination\EntityContentBase; use Drupal\migrate\Plugin\MigrationPluginManagerInterface; use Drupal\migrate_drupal_ui\Batch\MigrateUpgradeImportBatch; use Drupal\migrate_drupal\MigrationConfigurationTrait; @@ -100,6 +102,9 @@ public function buildForm(array $form, FormStateInterface $form_state) { case 'credentials': return $this->buildCredentialForm($form, $form_state); + case 'confirm_id_conflicts': + return $this->buildIdConflictForm($form, $form_state); + case 'confirm': return $this->buildConfirmForm($form, $form_state); @@ -434,6 +439,125 @@ public function validateCredentialForm(array &$form, FormStateInterface $form_st */ public function submitCredentialForm(array &$form, FormStateInterface $form_state) { // Indicate the next step is confirmation. + $form_state->set('step', 'confirm_id_conflicts'); + $form_state->setRebuild(); + } + + /** + * Confirmation form for ID conflicts. + * + * @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 buildIdConflictForm(array &$form, FormStateInterface $form_state) { + // Check if there are conflicts. If none, just skip this form! + $migration_ids = array_keys($form_state->get('migrations')); + $migrations = $this->pluginManager->createInstances($migration_ids); + + $auditor = new IdAuditor(); + $translated_content_conflicts = $content_conflicts = []; + foreach ($migrations as $migration) { + $audit = $auditor->audit($migration); + $destination_plugin = $migration->getDestinationPlugin(); + + if ($destination_plugin instanceof EntityContentBase && $destination_plugin->isTranslationDestination()) { + $translated_content_conflicts += $audit; + } + else { + $content_conflicts += $audit; + } + } + if (empty($content_conflicts) && empty($translated_content_conflicts)) { + $form_state->set('step', 'confirm'); + return $this->buildForm($form, $form_state); + } + + $form['warning'] = [ + '#type' => 'markup', + '#markup' => '

' . $this->t('Content may be overwritten') . '

', + ]; + $form = parent::buildForm($form, $form_state); + $form['actions']['submit']['#submit'] = ['::submitConfirmIdConflictForm']; + $form['actions']['submit']['#value'] = $this->t('I acknowledge I may lose data, continue anyway.'); + + $form = $this->conflictsForm($form, $form_state, $content_conflicts); + return $this->i18nWarningForm($form, $form_state, $translated_content_conflicts); + } + + /** + * Build the markup for conflict warnings. + * + * @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. + * @param array $content_conflicts + * The conflicted migration labels. + * + * @return array + * The form structure. + */ + protected function conflictsForm(array &$form, FormStateInterface $form_state, $content_conflicts) { + if (empty($content_conflicts)) { + return $form; + } + + $form['warning']['#markup'] .= '

' . $this->t('Upgrades work on clean and empty new installs of Drupal 8. However, it looks like you have content in your site that is unknown to the migrate system; perhaps manually added. These new entities may be overwritten if you run this upgrade. For more information, see the online handbook entry for handling migration conflicts.', [':id-conflicts-handbook' => 'https://www.drupal.org/docs/8/upgrade/known-issues-when-upgrading-from-drupal-6-or-7-to-drupal-8#id_conflicts']) . '

'; + + sort($content_conflicts); + $form['conflicts'] = [ + '#title' => $this->t('The conflicting content are of the following types:'), + '#theme' => 'item_list', + '#items' => $content_conflicts, + ]; + + return $form; + } + + /** + * Build the markup for i18n warnings. + * + * @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. + * @param array $translated_content_conflicts + * The conflicted translated migration labels. + * + * @return array + * The form structure. + */ + protected function i18nWarningForm(array &$form, FormStateInterface $form_state, $translated_content_conflicts) { + if (empty($translated_content_conflicts)) { + return $form; + } + + $form['warning']['#markup'] .= '

' . $this->t('It looks like you are migrating translated content. Be extra cautious and make sure you aren\'t introducting any conflicts. For more on the subject, see the online handbook entry for handling migration conflicts.', [':id-conflicts-handbook' => 'https://www.drupal.org/docs/8/upgrade/known-issues-when-upgrading-from-drupal-6-or-7-to-drupal-8#id_conflicts']) . '

'; + + sort($translated_content_conflicts); + $form['i18n'] = [ + '#title' => $this->t('The translated content are of the following types:'), + '#theme' => 'item_list', + '#items' => $translated_content_conflicts, + ]; + + 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 submitConfirmIdConflictForm(array &$form, FormStateInterface $form_state) { $form_state->set('step', 'confirm'); $form_state->setRebuild(); } diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php index e322aa3..46d8303 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php @@ -152,8 +152,10 @@ public function testMigrateUpgrade() { $this->drupalPostForm(NULL, $edits, t('Review upgrade')); $this->assertResponse(200); - $this->assertText('Are you sure?'); + $this->assertSession()->pageTextContains('Content may be overwritten'); + $this->drupalPostForm(NULL, [], t('I acknowledge I may lose data, continue anyway.')); // Ensure we get errors about missing modules. + $this->assertSession()->pageTextContains('Upgrade analysis report'); $this->assertSession()->pageTextContains(t('Source module not found for migration_provider_no_annotation.')); $this->assertSession()->pageTextContains(t('Source module not found for migration_provider_test.')); $this->assertSession()->pageTextContains(t('Destination module not found for migration_provider_test')); @@ -171,8 +173,10 @@ public function testMigrateUpgrade() { $this->drupalPostForm(NULL, $edits, t('Review upgrade')); $this->assertSession()->statusCodeEquals(200); - $this->assertSession()->pageTextContains('Are you sure?'); + $this->assertSession()->pageTextContains('Content may be overwritten'); + $this->drupalPostForm(NULL, [], t('I acknowledge I may lose data, continue anyway.')); // Ensure there are no errors about the missing modules from the test module. + $this->assertSession()->pageTextContains('Upgrade analysis report'); $this->assertSession()->pageTextNotContains(t('Source module not found for migration_provider_no_annotation.')); $this->assertSession()->pageTextNotContains(t('Source module not found for migration_provider_test.')); $this->assertSession()->pageTextNotContains(t('Destination module not found for migration_provider_test')); diff --git a/core/modules/node/migration_templates/d6_node.yml b/core/modules/node/migration_templates/d6_node.yml index 56d0459..84a4bf1 100644 --- a/core/modules/node/migration_templates/d6_node.yml +++ b/core/modules/node/migration_templates/d6_node.yml @@ -1,5 +1,6 @@ id: d6_node label: Nodes +audit: true migration_tags: - Drupal 6 deriver: Drupal\node\Plugin\migrate\D6NodeDeriver diff --git a/core/modules/node/migration_templates/d6_node_revision.yml b/core/modules/node/migration_templates/d6_node_revision.yml index f4ff301..74a42d4 100644 --- a/core/modules/node/migration_templates/d6_node_revision.yml +++ b/core/modules/node/migration_templates/d6_node_revision.yml @@ -1,5 +1,6 @@ id: d6_node_revision label: Node revisions +audit: true migration_tags: - Drupal 6 deriver: Drupal\node\Plugin\migrate\D6NodeDeriver diff --git a/core/modules/node/migration_templates/d7_node.yml b/core/modules/node/migration_templates/d7_node.yml index 359be81..8036797 100644 --- a/core/modules/node/migration_templates/d7_node.yml +++ b/core/modules/node/migration_templates/d7_node.yml @@ -1,5 +1,6 @@ id: d7_node label: Nodes +audit: true migration_tags: - Drupal 7 deriver: Drupal\node\Plugin\migrate\D7NodeDeriver diff --git a/core/modules/node/migration_templates/d7_node_revision.yml b/core/modules/node/migration_templates/d7_node_revision.yml index c6081ef..18c90b6 100644 --- a/core/modules/node/migration_templates/d7_node_revision.yml +++ b/core/modules/node/migration_templates/d7_node_revision.yml @@ -1,5 +1,6 @@ id: d7_node_revision label: Node revisions +audit: true migration_tags: - Drupal 7 deriver: Drupal\node\Plugin\migrate\D7NodeDeriver diff --git a/core/modules/taxonomy/migration_templates/d6_taxonomy_term.yml b/core/modules/taxonomy/migration_templates/d6_taxonomy_term.yml index e3c3e3d..9eafee5 100644 --- a/core/modules/taxonomy/migration_templates/d6_taxonomy_term.yml +++ b/core/modules/taxonomy/migration_templates/d6_taxonomy_term.yml @@ -1,5 +1,6 @@ id: d6_taxonomy_term label: Taxonomy terms +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/taxonomy/migration_templates/d6_term_node_revision.yml b/core/modules/taxonomy/migration_templates/d6_term_node_revision.yml index 91c8362..c3ebe30 100644 --- a/core/modules/taxonomy/migration_templates/d6_term_node_revision.yml +++ b/core/modules/taxonomy/migration_templates/d6_term_node_revision.yml @@ -1,5 +1,6 @@ id: d6_term_node_revision label: Term/node relationship revisions +audit: true migration_tags: - Drupal 6 deriver: Drupal\taxonomy\Plugin\migrate\D6TermNodeDeriver diff --git a/core/modules/taxonomy/migration_templates/d7_taxonomy_term.yml b/core/modules/taxonomy/migration_templates/d7_taxonomy_term.yml index 46f9f20..6033d4f 100644 --- a/core/modules/taxonomy/migration_templates/d7_taxonomy_term.yml +++ b/core/modules/taxonomy/migration_templates/d7_taxonomy_term.yml @@ -1,5 +1,6 @@ id: d7_taxonomy_term label: Taxonomy terms +audit: true migration_tags: - Drupal 7 deriver: Drupal\taxonomy\Plugin\migrate\D7TaxonomyTermDeriver diff --git a/core/modules/user/migration_templates/d6_user.yml b/core/modules/user/migration_templates/d6_user.yml index d58607b..35d31da 100644 --- a/core/modules/user/migration_templates/d6_user.yml +++ b/core/modules/user/migration_templates/d6_user.yml @@ -1,5 +1,6 @@ id: d6_user label: User accounts +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/user/migration_templates/d7_user.yml b/core/modules/user/migration_templates/d7_user.yml index 54c8805..c93288f 100644 --- a/core/modules/user/migration_templates/d7_user.yml +++ b/core/modules/user/migration_templates/d7_user.yml @@ -1,8 +1,9 @@ id: d7_user label: User accounts +audit: true migration_tags: - Drupal 7 -class: Drupal\user\Plugin\migrate\User +class: \Drupal\user\Plugin\migrate\User source: plugin: d7_user process: diff --git a/core/modules/user/src/Plugin/migrate/destination/EntityUser.php b/core/modules/user/src/Plugin/migrate/destination/EntityUser.php index 7c2c81a..aceb86d 100644 --- a/core/modules/user/src/Plugin/migrate/destination/EntityUser.php +++ b/core/modules/user/src/Plugin/migrate/destination/EntityUser.php @@ -127,4 +127,18 @@ protected function processStubRow(Row $row) { } } + /** + * {@inheritdoc} + */ + public function max() { + $highest_id = parent::max(); + + // Every Drupal site must have a user with UID of 1 and it's normal for + // migrations to overwrite this user. + if ($highest_id == 1) { + return 0; + } + return $highest_id; + } + }