diff --git a/core/modules/aggregator/migration_templates/d6_aggregator_feed.yml b/core/modules/aggregator/migration_templates/d6_aggregator_feed.yml index cad155374a..82b0dea04e 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 e14dbd60ed..83072547d6 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 5dbeb25eaf..27c9e7f6d4 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 054ba439f5..841ac7b3fb 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 55fbcb5c9d..8a39bc86c1 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 ca06cf04f9..3ac2d27d81 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 06820d462a..6c1d932aa7 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 94a2884477..da82992d76 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 8371d45f10..7945a9539b 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: Files +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 3fee046623..4c231b0d6c 100644 --- a/core/modules/file/migration_templates/d7_file.yml +++ b/core/modules/file/migration_templates/d7_file.yml @@ -1,7 +1,8 @@ # Every migration that references a file by Drupal 7 fid should specify this # migration as an optional dependency. id: d7_file -label: Files +label: Public Files +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 9c6b8e233c..02bc2a26ed 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: Files +label: Private Files +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 2c8ad4a45a..71bc1bcfb4 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 200a792047..a1729ed838 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' migration_tags: - Drupal 7 source: diff --git a/core/modules/migrate/src/Plugin/IdAuditingMigration.php b/core/modules/migrate/src/Plugin/IdAuditingMigration.php new file mode 100644 index 0000000000..661498ab58 --- /dev/null +++ b/core/modules/migrate/src/Plugin/IdAuditingMigration.php @@ -0,0 +1,30 @@ +getDestinationPlugin(); + $id_map = $this->getIdMap(); + if ($destination instanceof MaximumValueInterface && $id_map instanceof MaximumValueInterface && $destination->max() > $id_map->max()) { + $base_id = $this->getBaseId(); + $conflict[$base_id] = $base_id; + + $label = $this->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/Plugin/MaximumValueInterface.php b/core/modules/migrate/src/Plugin/MaximumValueInterface.php new file mode 100644 index 0000000000..fa37a9a317 --- /dev/null +++ b/core/modules/migrate/src/Plugin/MaximumValueInterface.php @@ -0,0 +1,9 @@ +configuration['translations']); } @@ -289,4 +287,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 b0db476987..336e765fa3 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 6bdd51ebf0..96159e6332 100644 --- a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php +++ b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php @@ -7,11 +7,13 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\migrate\MigrateMessage; +use Drupal\migrate\Plugin\MaximumValueInterface; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Event\MigrateIdMapMessageEvent; use Drupal\migrate\MigrateException; use Drupal\migrate\MigrateMessageInterface; use Drupal\migrate\Plugin\MigrateIdMapInterface; +use Drupal\migrate\Plugin\MigrationPluginManagerInterface; use Drupal\migrate\Row; use Drupal\migrate\Event\MigrateEvents; use Drupal\migrate\Event\MigrateMapSaveEvent; @@ -27,7 +29,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. @@ -42,6 +44,13 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP protected $eventDispatcher; /** + * The migration plugin manager. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface + */ + protected $migrationPluginManager; + + /** * The migration map table name. * * @var string @@ -152,11 +161,16 @@ 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. + * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager + * The migration plugin manager. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EventDispatcherInterface $event_dispatcher) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EventDispatcherInterface $event_dispatcher, MigrationPluginManagerInterface $migration_plugin_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->migration = $migration; $this->eventDispatcher = $event_dispatcher; + $this->migrationPluginManager = $migration_plugin_manager; $this->message = new MigrateMessage(); } @@ -169,7 +183,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 +940,59 @@ 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('TODO'); + } + $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, ':'))) { + $migrations = $this->migrationPluginManager->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 = $this->migrationPluginManager->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 b0fc0b8aad..022fef8c54 100644 --- a/core/modules/migrate/tests/src/Kernel/MigrateTestBase.php +++ b/core/modules/migrate/tests/src/Kernel/MigrateTestBase.php @@ -6,6 +6,7 @@ use Drupal\KernelTests\KernelTestBase; use Drupal\migrate\MigrateExecutable; use Drupal\migrate\MigrateMessageInterface; +use Drupal\migrate\Plugin\IdAuditingMigration; use Drupal\migrate\Plugin\MigrateIdMapInterface; use Drupal\migrate\Plugin\Migration; use Drupal\migrate\Plugin\MigrationInterface; @@ -248,4 +249,15 @@ protected function getMigration($plugin_id) { return $this->container->get('plugin.manager.migration')->createInstance($plugin_id); } + protected function auditAll(array $migrations) { + $conflicts = []; + foreach ($migrations as $migration) { + if ($migration instanceof IdAuditingMigration) { + $conflicts += $migration->audit(); + } + } + ksort($conflicts); + return $conflicts; + } + } diff --git a/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapEnsureTablesTest.php b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapEnsureTablesTest.php index 3c3cba8859..269728d973 100644 --- a/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapEnsureTablesTest.php +++ b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapEnsureTablesTest.php @@ -232,7 +232,8 @@ protected function runEnsureTablesTest($schema) { ->willReturn($plugin); /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher */ $event_dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); - $map = new TestSqlIdMap($database, [], 'sql', [], $migration, $event_dispatcher); + $migration_plugin_manager = $this->getMock('Drupal\migrate\Plugin\MigrationPluginManagerInterface'); + $map = new TestSqlIdMap($database, [], 'sql', [], $migration, $event_dispatcher, $migration_plugin_manager); $map->getDatabase(); } diff --git a/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php index 2ad2b3dc51..c636b09560 100644 --- a/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php +++ b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php @@ -111,8 +111,9 @@ protected function getIdMap() { ->method('getDestinationPlugin') ->willReturn($plugin); $event_dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $migration_plugin_manager = $this->getMock('Drupal\migrate\Plugin\MigrationPluginManagerInterface'); - $id_map = new TestSqlIdMap($this->database, [], 'sql', [], $migration, $event_dispatcher); + $id_map = new TestSqlIdMap($this->database, [], 'sql', [], $migration, $event_dispatcher, $migration_plugin_manager); $migration ->method('getIdMap') ->willReturn($id_map); diff --git a/core/modules/migrate/tests/src/Unit/TestSqlIdMap.php b/core/modules/migrate/tests/src/Unit/TestSqlIdMap.php index 6d3333848f..f3ee60ad18 100644 --- a/core/modules/migrate/tests/src/Unit/TestSqlIdMap.php +++ b/core/modules/migrate/tests/src/Unit/TestSqlIdMap.php @@ -6,6 +6,7 @@ use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\MigrateException; use Drupal\migrate\Plugin\migrate\id_map\Sql; +use Drupal\migrate\Plugin\MigrationPluginManagerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** @@ -28,10 +29,12 @@ class TestSqlIdMap extends Sql implements \Iterator { * The migration to do. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * The event dispatcher service. + * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager + * The migration plugin manager. */ - public function __construct(Connection $database, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EventDispatcherInterface $event_dispatcher) { + public function __construct(Connection $database, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EventDispatcherInterface $event_dispatcher, MigrationPluginManagerInterface $migration_plugin_manager) { $this->database = $database; - parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $event_dispatcher); + parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $event_dispatcher, $migration_plugin_manager); } /** 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 0000000000..24437296fe --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6AuditIdsTest.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 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($this->auditAll($migrations)); + } + + /** + * Tests all migrations with no ID conflicts. + */ + public function testAllMigrationsWithNoIdConflicts() { + $plugin_manager = $this->container->get('plugin.manager.migration'); + + // Audit all Drupal 6 migrations that support it. There should be no + // conflicts since no content has been created. + $conflicts = $this->auditAll($plugin_manager->createInstancesByTag('Drupal 6')); + $this->assertEmpty($conflicts); + } + + /** + * Tests all migrations with ID conflicts. + */ + public function testAllMigrationsWithIdConflicts() { + $plugin_manager = $this->container->get('plugin.manager.migration'); + + // Get all Drupal 6 migrations. + $migrations = $plugin_manager->createInstancesByTag('Drupal 6'); + + // Create content. + $this->createContent(); + + // Audit the IDs of all migrations. There should be conflicts since content + // has been created. + $conflicts = $this->auditAll($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 0000000000..6985c20a1b --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php @@ -0,0 +1,190 @@ +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($this->auditAll($migrations)); + } + + /** + * Tests all migrations with no ID conflicts. + */ + public function testAllMigrationsWithNoIdConflicts() { + $plugin_manager = $this->container->get('plugin.manager.migration'); + + // Audit the IDs of all Drupal 7 migrations. There should be no conflicts + // since no content has been created. + $conflicts = $this->auditAll($plugin_manager->createInstancesByTag('Drupal 7')); + $this->assertEmpty($conflicts); + } + + /** + * Tests all migrations with ID conflicts. + */ + public function testAllMigrationsWithIdConflicts() { + $plugin_manager = $this->container->get('plugin.manager.migration'); + + // Create content. + $this->createContent(); + + // Audit the IDs of all Drupal 7 migrations. There should be conflicts since + // content has been created. + $conflicts = $this->auditAll($plugin_manager->createInstancesByTag('Drupal 7')); + + $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 be0e2641c8..30d5483b6d 100644 --- a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php +++ b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php @@ -8,6 +8,7 @@ use Drupal\Core\Render\RendererInterface; use Drupal\Core\State\StateInterface; use Drupal\Core\Url; +use Drupal\migrate\Plugin\IdAuditingMigration; use Drupal\migrate\Plugin\MigrationPluginManagerInterface; use Drupal\migrate_drupal_ui\Batch\MigrateUpgradeImportBatch; use Drupal\migrate_drupal\MigrationConfigurationTrait; @@ -98,6 +99,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); @@ -430,6 +434,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); + + $i18n = $conflicts = []; + foreach ($migrations as $migration) { + if ($migration instanceof IdAuditingMigration) { + $audit = $migration->audit(); + + if ($migration->getDestinationPlugin()->isTranslationDestination()) { + $i18n += $audit; + } + else { + $conflicts += $audit; + } + } + } + if (empty($conflicts) && empty($i18n)) { + $form_state->set('step', 'confirm'); + return $this->buildForm($form, $form_state); + } + + $form['warning'] = [ + '#type' => 'markup', + '#markup' => '
' . $this->t('Upgrades work on brand new sites, or sites with previously upgraded data. 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($conflicts); + $form['conflicts'] = [ + '#title' => $this->t('The conflicting entities are of the following types:'), + '#theme' => 'item_list', + '#items' => $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 + * The internationalization migration labels. + * + * @return array + * The form structure. + */ + protected function i18nWarningForm(array &$form, FormStateInterface $form_state, $i18n) { + if (empty($i18n)) { + 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($i18n); + $form['i18n'] = [ + '#title' => $this->t('The translatable entities are of the following types:'), + '#theme' => 'item_list', + '#items' => $i18n, + ]; + + 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 5d2c605ca0..6c14d888b8 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php @@ -147,8 +147,10 @@ public function testMigrateUpgrade() { $this->drupalPostForm(NULL, $edits, t('Review upgrade')); $this->assertResponse(200); - $this->assertText('Are you sure?'); + $this->assertSession()->pageTextContains('Entities 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')); @@ -166,8 +168,10 @@ public function testMigrateUpgrade() { $this->drupalPostForm(NULL, $edits, t('Review upgrade')); $this->assertSession()->statusCodeEquals(200); - $this->assertSession()->pageTextContains('Are you sure?'); + $this->assertSession()->pageTextContains('Entities 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 56d0459a81..f2aa1e23b4 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 f4ff3011c4..a927c1a785 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 5de3055882..85b022160a 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 c6081ef110..32b8ba3edf 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 e3c3e3d342..f525fa6d32 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 91c8362e63..123a31a2d3 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 99004dfed6..29bde4b7ba 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 d58607b150..d3c808e4d5 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 +class: '\Drupal\migrate\Plugin\IdAuditingMigration' 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 ae523844b1..526e00d901 100644 --- a/core/modules/user/migration_templates/d7_user.yml +++ b/core/modules/user/migration_templates/d7_user.yml @@ -1,5 +1,6 @@ id: d7_user label: User accounts +class: '\Drupal\migrate\Plugin\IdAuditingMigration' migration_tags: - Drupal 7 class: Drupal\user\Plugin\migrate\User diff --git a/core/modules/user/src/Plugin/migrate/destination/EntityUser.php b/core/modules/user/src/Plugin/migrate/destination/EntityUser.php index 7c2c81af96..297f050109 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() { + $highestId = parent::max(); + + // Every Drupal site must have a user with UID of 1 and it's normal for + // migrations to overwrite this user. + if ($highestId == 1) { + return 0; + } + return $highestId; + } + }