diff --git a/core/modules/aggregator/migrations/d6_aggregator_feed.yml b/core/modules/aggregator/migrations/d6_aggregator_feed.yml index cad155374a..8689d5bcc8 100644 --- a/core/modules/aggregator/migrations/d6_aggregator_feed.yml +++ b/core/modules/aggregator/migrations/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/migrations/d6_aggregator_item.yml b/core/modules/aggregator/migrations/d6_aggregator_item.yml index e14dbd60ed..7c991eb761 100644 --- a/core/modules/aggregator/migrations/d6_aggregator_item.yml +++ b/core/modules/aggregator/migrations/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/migrations/d7_aggregator_feed.yml b/core/modules/aggregator/migrations/d7_aggregator_feed.yml index 5dbeb25eaf..48eb29de8c 100644 --- a/core/modules/aggregator/migrations/d7_aggregator_feed.yml +++ b/core/modules/aggregator/migrations/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/migrations/d7_aggregator_item.yml b/core/modules/aggregator/migrations/d7_aggregator_item.yml index 054ba439f5..342c5c8cbb 100644 --- a/core/modules/aggregator/migrations/d7_aggregator_item.yml +++ b/core/modules/aggregator/migrations/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/migrations/d6_custom_block.yml b/core/modules/block_content/migrations/d6_custom_block.yml index 55fbcb5c9d..071e4de19a 100644 --- a/core/modules/block_content/migrations/d6_custom_block.yml +++ b/core/modules/block_content/migrations/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/migrations/d7_custom_block.yml b/core/modules/block_content/migrations/d7_custom_block.yml index ca06cf04f9..1a9ea1978c 100644 --- a/core/modules/block_content/migrations/d7_custom_block.yml +++ b/core/modules/block_content/migrations/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/migrations/d6_comment.yml b/core/modules/comment/migrations/d6_comment.yml index 161820eeaa..afab0cb4ca 100644 --- a/core/modules/comment/migrations/d6_comment.yml +++ b/core/modules/comment/migrations/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/migrations/d7_comment.yml b/core/modules/comment/migrations/d7_comment.yml index dff4b64ab9..0837df91b7 100644 --- a/core/modules/comment/migrations/d7_comment.yml +++ b/core/modules/comment/migrations/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/migrations/d6_file.yml b/core/modules/file/migrations/d6_file.yml index 6544d7d2f6..5b8b672018 100644 --- a/core/modules/file/migrations/d6_file.yml +++ b/core/modules/file/migrations/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/migrations/d7_file.yml b/core/modules/file/migrations/d7_file.yml index b63f13e9e0..7e05a28aeb 100644 --- a/core/modules/file/migrations/d7_file.yml +++ b/core/modules/file/migrations/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/migrations/d7_file_private.yml b/core/modules/file/migrations/d7_file_private.yml index 197c701031..51de533867 100644 --- a/core/modules/file/migrations/d7_file_private.yml +++ b/core/modules/file/migrations/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/migrations/d6_menu_links.yml b/core/modules/menu_link_content/migrations/d6_menu_links.yml index 2c8ad4a45a..e05efee4a7 100644 --- a/core/modules/menu_link_content/migrations/d6_menu_links.yml +++ b/core/modules/menu_link_content/migrations/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/migrations/d7_menu_links.yml b/core/modules/menu_link_content/migrations/d7_menu_links.yml index 200a792047..81d4eb8530 100644 --- a/core/modules/menu_link_content/migrations/d7_menu_links.yml +++ b/core/modules/menu_link_content/migrations/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 0000000000..d2ef2eb5d9 --- /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/AuditResult.php b/core/modules/migrate/src/Audit/AuditResult.php new file mode 100644 index 0000000000..d5260775e1 --- /dev/null +++ b/core/modules/migrate/src/Audit/AuditResult.php @@ -0,0 +1,146 @@ +migration = $migration; + $this->status = $status; + array_walk($reasons, [$this, 'addReason']); + } + + /** + * Returns the audited migration. + * + * @return \Drupal\migrate\Plugin\MigrationInterface + * The audited migration. + */ + public function getMigration() { + return $this->migration; + } + + /** + * Returns the boolean result of the audit. + * + * @return bool + * The result of the audit. TRUE if the migration passed the audit, FALSE + * otherwise. + */ + public function passed() { + return $this->status; + } + + /** + * Adds a reason why the migration passed or failed the audit. + * + * @param string|object $reason + * The reason to add. Can be a string or a string-castable object. + * + * @return $this + */ + public function addReason($reason) { + array_push($this->reasons, (string) $reason); + return $this; + } + + /** + * Creates a passing audit result for a migration. + * + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The audited migration. + * @param string[] $reasons + * (optional) The reasons why the migration passed the audit. + * + * @return static + */ + public static function pass(MigrationInterface $migration, array $reasons = []) { + return new static($migration, TRUE, $reasons); + } + + /** + * Creates a failing audit result for a migration. + * + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The audited migration. + * @param array $reasons + * (optional) The reasons why the migration failed the audit. + * + * @return static + */ + public static function fail(MigrationInterface $migration, array $reasons = []) { + return new static($migration, FALSE, $reasons); + } + + /** + * Implements \Countable::count() for Twig template compatibility. + * + * @return int + * + * @see \Drupal\Component\Render\MarkupInterface + */ + public function count() { + return count($this->reasons); + } + + /** + * Returns the reasons the migration passed or failed, as a string. + * + * @return string + * + * @see \Drupal\Component\Render\MarkupInterface + */ + public function __toString() { + return implode("\n", $this->reasons); + } + + /** + * Returns the reasons the migration passed or failed, for JSON serialization. + * + * @return string[] + */ + public function jsonSerialize() { + return $this->reasons; + } + +} diff --git a/core/modules/migrate/src/Audit/AuditorInterface.php b/core/modules/migrate/src/Audit/AuditorInterface.php new file mode 100644 index 0000000000..a61b792cfe --- /dev/null +++ b/core/modules/migrate/src/Audit/AuditorInterface.php @@ -0,0 +1,42 @@ +getPluginDefinition(); + + // If the migration does not opt into auditing, it passes. + // @todo Use $migration->isAuditable() when + // https://www.drupal.org/project/drupal/issues/2930832 is in. + if (empty($plugin_definition['audit'])) { + return AuditResult::pass($migration); + } + + $interface = HighestIdInterface::class; + + $destination = $migration->getDestinationPlugin(); + if (!$destination instanceof HighestIdInterface) { + throw new AuditException($migration, "Destination does not implement $interface"); + } + + $id_map = $migration->getIdMap(); + if (!$id_map instanceof HighestIdInterface) { + throw new AuditException($migration, "ID map does not implement $interface"); + } + + if ($destination->getHighestId() > $id_map->getHighestId()) { + return AuditResult::fail($migration, [ + $this->t('The destination system contains data which was not created by a migration.'), + ]); + } + return AuditResult::pass($migration); + } + + /** + * {@inheritdoc} + */ + public function auditMultiple(array $migrations) { + $conflicts = []; + + foreach ($migrations as $migration) { + $migration_id = $migration->getPluginId(); + $conflicts[$migration_id] = $this->audit($migration); + } + ksort($conflicts); + return $conflicts; + } + +} diff --git a/core/modules/migrate/src/Plugin/Migration.php b/core/modules/migrate/src/Plugin/Migration.php index 3bbe38ad74..92f7864d5a 100644 --- a/core/modules/migrate/src/Plugin/Migration.php +++ b/core/modules/migrate/src/Plugin/Migration.php @@ -155,6 +155,17 @@ class Migration extends PluginBase implements MigrationInterface, RequirementsIn protected $migration_tags = []; /** + * Whether the migration is auditable. + * + * If set to TRUE, the migration's IDs will be audited. This means that, if + * the highest destination ID is greater than the highest source ID, a warning + * will be displayed that entities might be overwritten. + * + * @var bool + */ + protected $audit = FALSE; + + /** * These migrations, if run, must be executed before this migration. * * These are different from the configuration dependencies. Migration diff --git a/core/modules/migrate/src/Plugin/migrate/destination/Entity.php b/core/modules/migrate/src/Plugin/migrate/destination/Entity.php index 12b0ed6d31..0cebbd07c8 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/Entity.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/Entity.php @@ -94,6 +94,10 @@ * The list of bundles this entity type has. */ public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles) { + $plugin_definition += [ + 'label' => $storage->getEntityType()->getPluralLabel(), + ]; + parent::__construct($configuration, $plugin_id, $plugin_definition, $migration); $this->storage = $storage; $this->bundles = $bundles; diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php index cf3ae1dedb..cf17246ea9 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php @@ -9,6 +9,7 @@ use Drupal\Core\Field\FieldTypePluginManagerInterface; use Drupal\Core\TypedData\TranslatableInterface; use Drupal\Core\TypedData\TypedDataInterface; +use Drupal\migrate\Audit\HighestIdInterface; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\MigrateException; use Drupal\migrate\Plugin\MigrateIdMapInterface; @@ -18,7 +19,7 @@ /** * The destination class for all content entities lacking a specific class. */ -class EntityContentBase extends Entity { +class EntityContentBase extends Entity implements HighestIdInterface { /** * Entity manager. @@ -111,12 +112,9 @@ protected function save(ContentEntityInterface $entity, array $old_destination_i } /** - * Get whether this destination is for translations. - * - * @return bool - * Whether this destination is for translations. + * {@inheritdoc} */ - protected function isTranslationDestination() { + public function isTranslationDestination() { return !empty($this->configuration['translations']); } @@ -294,4 +292,15 @@ protected function getDefinitionFromEntity($key) { ] + $field_definition->getSettings(); } + /** + * {@inheritdoc} + */ + public function getHighestId() { + $values = $this->storage->getQuery() + ->sort($this->getKey('id'), 'DESC') + ->range(0, 1) + ->execute(); + return (int) current($values); + } + } diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php index b0db476987..290c1e1b5a 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php @@ -3,7 +3,12 @@ namespace Drupal\migrate\Plugin\migrate\destination; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Field\FieldTypePluginManagerInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\migrate\MigrateException; +use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Row; /** @@ -19,6 +24,16 @@ class EntityRevision extends EntityContentBase { /** * {@inheritdoc} */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager) { + $plugin_definition += [ + 'label' => new TranslatableMarkup('@entity_type revisions', ['@entity_type' => $storage->getEntityType()->getSingularLabel()]), + ]; + parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager, $field_type_manager); + } + + /** + * {@inheritdoc} + */ protected static function getEntityTypeId($plugin_id) { // Remove entity_revision: return substr($plugin_id, 16); @@ -78,4 +93,18 @@ public function getIds() { throw new MigrateException('This entity type does not support revisions.'); } + /** + * {@inheritdoc} + */ + public function getHighestId() { + $values = $this->storage->getQuery() + ->allRevisions() + ->sort($this->getKey('revision'), 'DESC') + ->range(0, 1) + ->execute(); + // The array keys are the revision IDs. + // The array contains only one entry, so we can use key(). + return (int) key($values); + } + } 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..8bc17f19f5 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\HighestIdInterface; 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, HighestIdInterface { /** * 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); @@ -925,4 +928,69 @@ public function valid() { return $this->currentRow !== FALSE; } + /** + * Returns the migration plugin manager. + * + * @todo Inject as a dependency in https://www.drupal.org/node/2919158. + * + * @return \Drupal\migrate\Plugin\MigrationPluginManagerInterface + * The migration plugin manager. + */ + protected function getMigrationPluginManager() { + return \Drupal::service('plugin.manager.migration'); + } + + /** + * {@inheritdoc} + */ + public function getHighestId() { + array_filter( + $this->migration->getDestinationPlugin()->getIds(), + function (array $id) { + if ($id['type'] !== 'integer') { + throw new \LogicException('Cannot determine the highest migrated ID without an integer ID column'); + } + } + ); + + // List of mapping tables to look in for the highest ID. + $map_tables = [ + $this->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($this->migration->id(), 0, strpos($this->migration->id(), static::DERIVATIVE_SEPARATOR))) { + $migration_manager = $this->getMigrationPluginManager(); + $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', $this->destinationIdFields()) + ->range(0, 1); + foreach (array_values($this->destinationIdFields()) as $order_field) { + $query->orderBy($order_field, 'DESC'); + } + $ids[] = $query->execute()->fetchField(); + } + + // Return the highest of all the mapped IDs. + return (int) max($ids); + } + } diff --git a/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php index 4fa08fed0a..9f43523560 100644 --- a/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php +++ b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php @@ -8,9 +8,9 @@ namespace Drupal\Tests\migrate\Unit\Plugin\migrate\destination; use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\ContentEntityType; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldTypePluginManagerInterface; use Drupal\migrate\MigrateException; @@ -39,6 +39,11 @@ class EntityContentBaseTest extends UnitTestCase { protected $storage; /** + * @var \Drupal\Core\Entity\EntityTypeInterface + */ + protected $entityType; + + /** * @var \Drupal\Core\Entity\EntityManagerInterface */ protected $entityManager; @@ -51,6 +56,11 @@ protected function setUp() { $this->migration = $this->prophesize(MigrationInterface::class); $this->storage = $this->prophesize(EntityStorageInterface::class); + + $this->entityType = $this->prophesize(EntityTypeInterface::class); + $this->entityType->getPluralLabel()->willReturn('wonkiness'); + $this->storage->getEntityType()->willReturn($this->entityType->reveal()); + $this->entityManager = $this->prophesize(EntityManagerInterface::class); } @@ -104,14 +114,11 @@ public function testImportEntityLoadFailure() { */ public function testUntranslatable() { // An entity type without a language. - $entity_type = $this->prophesize(ContentEntityType::class); - $entity_type->getKey('langcode')->willReturn(''); - $entity_type->getKey('id')->willReturn('id'); + $this->entityType->getKey('langcode')->willReturn(''); + $this->entityType->getKey('id')->willReturn('id'); $this->entityManager->getBaseFieldDefinitions('foo') ->willReturn(['id' => BaseFieldDefinitionTest::create('integer')]); - $this->storage->getEntityType()->willReturn($entity_type->reveal()); - $destination = new EntityTestDestination( ['translations' => TRUE], '', diff --git a/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php b/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php index f68c2bea1f..8a6fe9a43d 100644 --- a/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php +++ b/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php @@ -9,6 +9,7 @@ use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Plugin\migrate\destination\EntityRevision as RealEntityRevision; use Drupal\migrate\Row; @@ -48,6 +49,12 @@ protected function setUp() { // Setup mocks to be used when creating a revision destination. $this->migration = $this->prophesize(MigrationInterface::class); $this->storage = $this->prophesize('\Drupal\Core\Entity\EntityStorageInterface'); + + $entity_type = $this->prophesize(EntityTypeInterface::class); + $entity_type->getSingularLabel()->willReturn('crazy'); + $entity_type->getPluralLabel()->willReturn('craziness'); + $this->storage->getEntityType()->willReturn($entity_type->reveal()); + $this->entityManager = $this->prophesize('\Drupal\Core\Entity\EntityManagerInterface'); $this->fieldTypeManager = $this->prophesize('\Drupal\Core\Field\FieldTypePluginManagerInterface'); } 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..256e469d13 --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6AuditIdsTest.php @@ -0,0 +1,176 @@ +coreModuleListDataProvider()); + parent::setUp(); + + // Install required entity schemas. + $this->installEntitySchemas(); + + // Install required schemas. + $this->installSchema('book', ['book']); + $this->installSchema('dblog', ['watchdog']); + $this->installSchema('forum', ['forum_index']); + $this->installSchema('node', ['node_access']); + $this->installSchema('search', ['search_dataset']); + $this->installSchema('tracker', ['tracker_node', 'tracker_user']); + + // Enable content moderation for nodes of type page. + $this->installEntitySchema('content_moderation_state'); + $this->installConfig('content_moderation'); + NodeType::create(['type' => 'page'])->save(); + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page'); + $workflow->save(); + } + + /** + * Tests multiple migrations to the same destination with no ID conflicts. + */ + public function testMultipleMigrationWithoutIdConflicts() { + // Create a node of type page. + $node = Node::create(['type' => 'page', 'title' => 'foo']); + $node->moderation_state->value = 'published'; + $node->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'), + ]; + + $results = (new IdAuditor())->auditMultiple($migrations); + /** @var \Drupal\migrate\Audit\AuditResult $result */ + foreach ($results as $result) { + $this->assertInstanceOf(AuditResult::class, $result); + $this->assertTrue($result->passed()); + } + } + + /** + * 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. + $results = (new IdAuditor())->auditMultiple($migrations); + /** @var \Drupal\migrate\Audit\AuditResult $result */ + foreach ($results as $result) { + $this->assertInstanceOf(AuditResult::class, $result); + $this->assertTrue($result->passed()); + } + } + + /** + * 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 = array_map( + function (AuditResult $result) { + return $result->passed() ? NULL : $result->getMigration()->getBaseId(); + }, + (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->assertEmpty(array_diff(array_filter($conflicts), $expected)); + } + + /** + * Tests draft revisions ID conflicts. + */ + public function testDraftRevisionIdConflicts() { + // Create a published node of type page. + $node = Node::create(['type' => 'page', 'title' => 'foo']); + $node->moderation_state->value = 'published'; + $node->save(); + + // Create a draft revision. + $node->moderation_state->value = 'draft'; + $node->setNewRevision(TRUE); + $node->save(); + + // Insert data in the d6_node_revision:page migration mappping table to + // simulate a previously migrated node revison. + $table_name = $this->getMigration('d6_node_revision: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_revision migration. There should be + // conflicts since a draft revision has been created. + /** @var \Drupal\migrate\Audit\AuditResult $result */ + $result = (new IdAuditor())->audit($this->getMigration('d6_node_revision:page')); + $this->assertInstanceOf(AuditResult::class, $result); + $this->assertFalse($result->passed()); + } + +} 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..377985a331 --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php @@ -0,0 +1,175 @@ +coreModuleListDataProvider()); + parent::setUp(); + + // Install required entity schemas. + $this->installEntitySchemas(); + + // Install required schemas. + $this->installSchema('book', ['book']); + $this->installSchema('dblog', ['watchdog']); + $this->installSchema('forum', ['forum_index']); + $this->installSchema('node', ['node_access']); + $this->installSchema('search', ['search_dataset']); + $this->installSchema('tracker', ['tracker_node', 'tracker_user']); + + // Enable content moderation for nodes of type page. + $this->installEntitySchema('content_moderation_state'); + $this->installConfig('content_moderation'); + NodeType::create(['type' => 'page'])->save(); + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page'); + $workflow->save(); + } + + /** + * Tests multiple migrations to the same destination with no ID conflicts. + */ + public function testMultipleMigrationWithoutIdConflicts() { + // Create a node of type page. + $node = Node::create(['type' => 'page', 'title' => 'foo']); + $node->moderation_state->value = 'published'; + $node->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'), + ]; + + $results = (new IdAuditor())->auditMultiple($migrations); + /** @var \Drupal\migrate\Audit\AuditResult $result */ + foreach ($results as $result) { + $this->assertInstanceOf(AuditResult::class, $result); + $this->assertTrue($result->passed()); + } + } + + /** + * 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. + $results = (new IdAuditor())->auditMultiple($migrations); + /** @var \Drupal\migrate\Audit\AuditResult $result */ + foreach ($results as $result) { + $this->assertInstanceOf(AuditResult::class, $result); + $this->assertTrue($result->passed()); + } + } + + /** + * 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 = array_map( + function (AuditResult $result) { + return $result->passed() ? NULL : $result->getMigration()->getBaseId(); + }, + (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->assertEmpty(array_diff(array_filter($conflicts), $expected)); + } + + /** + * Tests draft revisions ID conflicts. + */ + public function testDraftRevisionIdConflicts() { + // Create a published node of type page. + $node = Node::create(['type' => 'page', 'title' => 'foo']); + $node->moderation_state->value = 'published'; + $node->save(); + + // Create a draft revision. + $node->moderation_state->value = 'draft'; + $node->setNewRevision(TRUE); + $node->save(); + + // Insert data in the d7_node_revision:page migration mappping table to + // simulate a previously migrated node revison. + $table_name = $this->getMigration('d7_node_revision: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_revision migration. There should be + // conflicts since a draft revision has been created. + /** @var \Drupal\migrate\Audit\AuditResult $result */ + $result = (new IdAuditor())->audit($this->getMigration('d7_node_revision:page')); + $this->assertInstanceOf(AuditResult::class, $result); + $this->assertFalse($result->passed()); + } + +} diff --git a/core/modules/migrate_drupal/tests/src/Traits/CreateTestContentEntitiesTrait.php b/core/modules/migrate_drupal/tests/src/Traits/CreateTestContentEntitiesTrait.php new file mode 100644 index 0000000000..db720e8cb7 --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Traits/CreateTestContentEntitiesTrait.php @@ -0,0 +1,131 @@ +installEntitySchema('aggregator_feed'); + $this->installEntitySchema('aggregator_item'); + $this->installEntitySchema('block_content'); + $this->installEntitySchema('comment'); + $this->installEntitySchema('file'); + $this->installEntitySchema('menu_link_content'); + $this->installEntitySchema('node'); + $this->installEntitySchema('taxonomy_term'); + $this->installEntitySchema('user'); + } + + /** + * Create several pieces of generic content. + */ + 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 6bfaf42f1d..898a8a5568 100644 --- a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php +++ b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php @@ -9,6 +9,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\Plugin\MigrateFieldPluginManagerInterface; use Drupal\migrate_drupal_ui\Batch\MigrateUpgradeImportBatch; @@ -124,6 +126,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); @@ -457,6 +462,155 @@ 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); + + $translated_content_conflicts = $content_conflicts = []; + + $results = (new IdAuditor())->auditMultiple($migrations); + + /** @var \Drupal\migrate\Audit\AuditResult $result */ + foreach ($results as $result) { + $destination = $result->getMigration()->getDestinationPlugin(); + if ($destination instanceof EntityContentBase && $destination->isTranslationDestination()) { + // Translations are not yet supperted by the audit system. For now, we + // only warn the user to be cautious when migrating translated content. + // I18n support should be added in https://www.drupal.org/node/2905759. + $translated_content_conflicts[] = $result; + } + elseif (!$result->passed()) { + $content_conflicts[] = $result; + } + + } + if (empty($content_conflicts) && empty($translated_content_conflicts)) { + $form_state->set('step', 'confirm'); + return $this->buildForm($form, $form_state); + } + + drupal_set_message($this->t('WARNING: Content may be overwritten on your new site.'), 'warning'); + + $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.'); + + if ($content_conflicts) { + $form = $this->conflictsForm($form, $form_state, $content_conflicts); + } + if ($translated_content_conflicts) { + $form = $this->i18nWarningForm($form, $form_state, $translated_content_conflicts); + } + return $form; + } + + /** + * 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 \Drupal\migrate\Audit\AuditResult[] $conflicts + * The failing audit results. + * + * @return array + * The form structure. + */ + protected function conflictsForm(array &$form, FormStateInterface $form_state, array $conflicts) { + $form['conflicts'] = [ + '#title' => $this->t('There is conflicting content of these types:'), + '#theme' => 'item_list', + '#items' => $this->formatConflicts($conflicts), + ]; + + $form['warning'] = [ + '#type' => 'markup', + '#markup' => '

' . $this->t('It looks like you have content on your new site which may be overwritten if you continue to run this upgrade. The upgrade should be performed on a clean Drupal 8 installation. For more information see the upgrade handbook.', [':id-conflicts-handbook' => 'https://www.drupal.org/docs/8/upgrade/known-issues-when-upgrading-from-drupal-6-or-7-to-drupal-8#id_conflicts']) . '

', + ]; + + return $form; + } + + /** + * Formats a set of failing audit results as strings. + * + * Each string is the label of the destination plugin of the migration that + * failed the audit, keyed by the destination plugin ID in order to prevent + * duplication. + * + * @param \Drupal\migrate\Audit\AuditResult[] $conflicts + * The failing audit results. + * + * @return string[] + * The formatted audit results. + */ + protected function formatConflicts(array $conflicts) { + $items = []; + + foreach ($conflicts as $conflict) { + $definition = $conflict->getMigration()->getDestinationPlugin()->getPluginDefinition(); + $id = $definition['id']; + $items[$id] = $definition['label']; + } + sort($items, SORT_STRING); + + return $items; + } + + /** + * 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 \Drupal\migrate\Audit\AuditResult[] $conflicts + * The failing audit results. + * + * @return array + * The form structure. + */ + protected function i18nWarningForm(array &$form, FormStateInterface $form_state, array $conflicts) { + $form['i18n'] = [ + '#title' => $this->t('There is translated content of these types:'), + '#theme' => 'item_list', + '#items' => $this->formatConflicts($conflicts), + ]; + + $form['i18n_warning'] = [ + '#type' => 'markup', + '#markup' => '

' . $this->t('It looks like you are migrating translated content from your old site. Possible ID conflicts for translations are not automatically detected in the current version of Drupal. Refer to the upgrade handbook for instructions on how to avoid ID conflicts with translated content.', [':id-conflicts-handbook' => 'https://www.drupal.org/docs/8/upgrade/known-issues-when-upgrading-from-drupal-6-or-7-to-drupal-8#id_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 d5cc93908a..98c05aaef4 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php @@ -6,12 +6,15 @@ use Drupal\migrate\Plugin\MigrateIdMapInterface; use Drupal\migrate_drupal\MigrationConfigurationTrait; use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\migrate_drupal\Traits\CreateTestContentEntitiesTrait; /** * Provides a base class for testing migration upgrades in the UI. */ abstract class MigrateUpgradeTestBase extends BrowserTestBase { + use MigrationConfigurationTrait; + use CreateTestContentEntitiesTrait; /** * Use the Standard profile to test help implementations of many core modules. @@ -54,6 +57,9 @@ protected function setUp() { // Log in as user 1. Migrations in the UI can only be performed as user 1. $this->drupalLogin($this->rootUser); + + // Create content. + $this->createContent(); } /** @@ -116,7 +122,8 @@ protected function tearDown() { public function testMigrateUpgrade() { $connection_options = $this->sourceDatabase->getConnectionOptions(); $this->drupalGet('/upgrade'); - $this->assertSession()->responseContains('Upgrade a site by importing its files and the data from its database into a clean and empty new install of Drupal 8.'); + $session = $this->assertSession(); + $session->responseContains('Upgrade a site by importing its files and the data from its database into a clean and empty new install of Drupal 8.'); $this->drupalPostForm(NULL, [], t('Continue')); $this->assertText('Provide credentials for the database of the Drupal site you want to upgrade.'); @@ -153,37 +160,52 @@ public function testMigrateUpgrade() { $this->assertText('Resolve the issue below to continue the upgrade.'); $this->drupalPostForm(NULL, $edits, t('Review upgrade')); + $session->pageTextContains('WARNING: Content may be overwritten on your new site.'); + $session->pageTextContains('There is conflicting content of these types:'); + $session->pageTextContains('aggregator feed entities'); + $session->pageTextContains('aggregator feed item entities'); + $session->pageTextContains('custom block entities'); + $session->pageTextContains('custom menu link entities'); + $session->pageTextContains('file entities'); + $session->pageTextContains('taxonomy term entities'); + $session->pageTextContains('user entities'); + $session->pageTextContains('comments'); + $session->pageTextContains('content item revisions'); + $session->pageTextContains('content items'); + $session->pageTextContains('There is translated content of these types:'); + $this->drupalPostForm(NULL, [], t('I acknowledge I may lose data. Continue anyway.')); $this->assertResponse(200); $this->assertText('Upgrade analysis report'); // Ensure we get errors about missing modules. - $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')); + $session->pageTextContains(t('Source module not found for migration_provider_no_annotation.')); + $session->pageTextContains(t('Source module not found for migration_provider_test.')); + $session->pageTextContains(t('Destination module not found for migration_provider_test')); // Uninstall the module causing the missing module error messages. $this->container->get('module_installer')->uninstall(['migration_provider_test'], TRUE); // Restart the upgrade process. $this->drupalGet('/upgrade'); - $this->assertSession()->responseContains('Upgrade a site by importing its files and the data from its database into a clean and empty new install of Drupal 8.'); + $session->responseContains('Upgrade a site by importing its files and the data from its database into a clean and empty new install of Drupal 8.'); $this->drupalPostForm(NULL, [], t('Continue')); - $this->assertSession()->pageTextContains('Provide credentials for the database of the Drupal site you want to upgrade.'); - $this->assertSession()->fieldExists('mysql[host]'); + $session->pageTextContains('Provide credentials for the database of the Drupal site you want to upgrade.'); + $session->fieldExists('mysql[host]'); $this->drupalPostForm(NULL, $edits, t('Review upgrade')); - $this->assertSession()->statusCodeEquals(200); - $this->assertSession()->pageTextContains('Upgrade analysis report'); + $session->pageTextContains('WARNING: Content may be overwritten on your new site.'); + $this->drupalPostForm(NULL, [], t('I acknowledge I may lose data. Continue anyway.')); + $session->statusCodeEquals(200); + $session->pageTextContains('Upgrade analysis report'); // Ensure there are no errors about the missing modules from the test module. - $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')); + $session->pageTextNotContains(t('Source module not found for migration_provider_no_annotation.')); + $session->pageTextNotContains(t('Source module not found for migration_provider_test.')); + $session->pageTextNotContains(t('Destination module not found for migration_provider_test')); // Ensure there are no errors about any other missing migration providers. - $this->assertSession()->pageTextNotContains(t('module not found')); + $session->pageTextNotContains(t('module not found')); // Test the available migration paths. $all_available = $this->getAvailablePaths(); - $session = $this->assertSession(); foreach ($all_available as $available) { $session->elementExists('xpath', "//span[contains(@class, 'checked') and text() = '$available']"); $session->elementNotExists('xpath', "//span[contains(@class, 'warning') and text() = '$available']"); diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php index 3717606cd8..432b7036d7 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php @@ -35,7 +35,7 @@ protected function getSourceBasePath() { protected function getEntityCounts() { return [ 'aggregator_item' => 1, - 'aggregator_feed' => 1, + 'aggregator_feed' => 2, 'block' => 35, 'block_content' => 2, 'block_content_type' => 1, @@ -48,7 +48,7 @@ protected function getEntityCounts() { 'editor' => 2, 'field_config' => 84, 'field_storage_config' => 58, - 'file' => 7, + 'file' => 8, 'filter_format' => 7, 'image_style' => 5, 'language_content_settings' => 2, @@ -68,7 +68,7 @@ protected function getEntityCounts() { 'tour' => 4, 'user' => 7, 'user_role' => 6, - 'menu_link_content' => 4, + 'menu_link_content' => 5, 'view' => 16, 'date_format' => 11, 'entity_form_display' => 29, diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php index 3cfe4bc6d7..608967c754 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php @@ -39,7 +39,7 @@ protected function getSourceBasePath() { */ protected function getEntityCounts() { return [ - 'aggregator_item' => 10, + 'aggregator_item' => 11, 'aggregator_feed' => 1, 'block' => 25, 'block_content' => 1, @@ -72,7 +72,7 @@ protected function getEntityCounts() { 'tour' => 4, 'user' => 4, 'user_role' => 3, - 'menu_link_content' => 7, + 'menu_link_content' => 8, 'view' => 16, 'date_format' => 11, 'entity_form_display' => 17, diff --git a/core/modules/node/migrations/d6_node.yml b/core/modules/node/migrations/d6_node.yml index 56d0459a81..84a4bf1872 100644 --- a/core/modules/node/migrations/d6_node.yml +++ b/core/modules/node/migrations/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/migrations/d6_node_revision.yml b/core/modules/node/migrations/d6_node_revision.yml index f4ff3011c4..74a42d4307 100644 --- a/core/modules/node/migrations/d6_node_revision.yml +++ b/core/modules/node/migrations/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/migrations/d7_node.yml b/core/modules/node/migrations/d7_node.yml index 359be81cc4..80367971a3 100644 --- a/core/modules/node/migrations/d7_node.yml +++ b/core/modules/node/migrations/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/migrations/d7_node_revision.yml b/core/modules/node/migrations/d7_node_revision.yml index c6081ef110..18c90b6f49 100644 --- a/core/modules/node/migrations/d7_node_revision.yml +++ b/core/modules/node/migrations/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/migrations/d6_taxonomy_term.yml b/core/modules/taxonomy/migrations/d6_taxonomy_term.yml index e3c3e3d342..9eafee544c 100644 --- a/core/modules/taxonomy/migrations/d6_taxonomy_term.yml +++ b/core/modules/taxonomy/migrations/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/migrations/d6_term_node_revision.yml b/core/modules/taxonomy/migrations/d6_term_node_revision.yml index 91c8362e63..c3ebe3059f 100644 --- a/core/modules/taxonomy/migrations/d6_term_node_revision.yml +++ b/core/modules/taxonomy/migrations/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/migrations/d7_taxonomy_term.yml b/core/modules/taxonomy/migrations/d7_taxonomy_term.yml index 46f9f20427..6033d4f875 100644 --- a/core/modules/taxonomy/migrations/d7_taxonomy_term.yml +++ b/core/modules/taxonomy/migrations/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/migrations/d6_user.yml b/core/modules/user/migrations/d6_user.yml index d58607b150..35d31da875 100644 --- a/core/modules/user/migrations/d6_user.yml +++ b/core/modules/user/migrations/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/migrations/d7_user.yml b/core/modules/user/migrations/d7_user.yml index 54c8805875..ee70db782e 100644 --- a/core/modules/user/migrations/d7_user.yml +++ b/core/modules/user/migrations/d7_user.yml @@ -1,5 +1,6 @@ id: d7_user label: User accounts +audit: true 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 aa74b84a43..b5317beb8c 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 getHighestId() { + $highest_id = parent::getHighestId(); + + // 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; + } + }