diff --git a/core/modules/migrate/migrate.services.yml b/core/modules/migrate/migrate.services.yml index 1a4f64d..1b95191 100644 --- a/core/modules/migrate/migrate.services.yml +++ b/core/modules/migrate/migrate.services.yml @@ -30,3 +30,5 @@ services: plugin.manager.migration: class: Drupal\migrate\Plugin\MigrationPluginManager arguments: ['@module_handler', '@cache.discovery_migration', '@language_manager'] + migrate.id_auditor: + class: Drupal\migrate\MigrateIdAuditor diff --git a/core/modules/migrate/src/MigrateIdAuditor.php b/core/modules/migrate/src/MigrateIdAuditor.php new file mode 100644 index 0000000..0b6a3b9 --- /dev/null +++ b/core/modules/migrate/src/MigrateIdAuditor.php @@ -0,0 +1,36 @@ +getDestinationPlugin(); + if ($destination instanceof MigrateIdAuditInterface) { + if ($destination->unsafeIdsExist($migration->getIdMap())) { + $ret[$destination->entityTypeId()] = TRUE; + } + } + } + return array_keys($ret); + } + +} diff --git a/core/modules/migrate/src/Plugin/MigrateIdAuditInterface.php b/core/modules/migrate/src/Plugin/MigrateIdAuditInterface.php new file mode 100644 index 0000000..6837068 --- /dev/null +++ b/core/modules/migrate/src/Plugin/MigrateIdAuditInterface.php @@ -0,0 +1,39 @@ +getSettings(); } + /** + * Get the highest ID that exists for this destination. + * + * This is not the highest ID that has been migrated, but the highest ID + * that exists in the destination, eg: highest node ID on the site. + * + * @return int + * The highest ID value found. If no IDs at all are found, or if the + * concept of a highest ID is not meaningful, zero should be returned. + */ + protected function highestDestinationId() { + $query = $this->storage->getQuery() + ->sort($this->getHighestIdField(), 'DESC') + ->range(0, 1); + $found = $query->execute(); + return (int) reset($found); + } + + /** + * {@inheritdoc} + */ + public function getHighestIdField() { + return $this->getKey('id'); + } + + /** + * Check whether unsafe IDs exist that should inhibit migration. + * + * @param \Drupal\migrate\Plugin\MigrateIdMapInterface $idMap + * The ID map for this migration. + * + * @return bool + * Whether unsafe IDs exist. + */ + public function unsafeIdsExist(MigrateIdMapInterface $idMap) { + if (!empty($this->migration->getPluginDefinition()['preserves_ids'])) { + // If IDs are not preserved, we can't have conflicts. + return FALSE; + } + + if (!($idMap instanceof MigrateMaxIdInterface)) { + // We don't know how to audit IDs without a cooperating ID map. + return FALSE; + } + + $highestMigrated = $idMap->getMaxId($this->getHighestIdField()); + if ($this->highestDestinationId() > $highestMigrated) { + // There's a new ID that we might conflict with! + return TRUE; + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function entityTypeId() { + return $this->storage->getEntityTypeId(); + } + } diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php index b0db476..b646059 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php @@ -78,4 +78,11 @@ public function getIds() { throw new MigrateException('This entity type does not support revisions.'); } + /** + * {@inheritdoc} + */ + public function getHighestIdField() { + return $this->getKey('revision'); + } + } 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 27b7569..2b6e30c 100644 --- a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php +++ b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php @@ -6,6 +6,7 @@ use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginBase; +use Drupal\migrate\Plugin\MigrateMaxIdInterface; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Event\MigrateIdMapMessageEvent; use Drupal\migrate\MigrateException; @@ -26,7 +27,7 @@ * * @PluginID("sql") */ -class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryPluginInterface { +class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryPluginInterface, MigrateMaxIdInterface { /** * Column name of hashed source id values. @@ -923,4 +924,22 @@ public function valid() { return $this->currentRow !== FALSE; } + /** + * {@inheritdoc} + */ + public function getMaxId($field) { + if (!$this->getDatabase()->schema()->tableExists($this->mapTableName())) { + return 0; + } + + $sqlField = $this->destinationIdFields()[$field]; + + $query = $this->getDatabase()->select($this->mapTableName(), 'map') + ->fields('map', [$sqlField]) + ->orderBy($sqlField, 'DESC') + ->range(0, 1); + $found = $query->execute()->fetchField(); + return (int) $found; + } + } diff --git a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php index c87247e..d757647 100644 --- a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php +++ b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php @@ -3,11 +3,13 @@ namespace Drupal\migrate_drupal_ui\Form; use Drupal\Core\Datetime\DateFormatterInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\ConfirmFormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\State\StateInterface; use Drupal\Core\Url; +use Drupal\migrate\MigrateIdAuditor; use Drupal\migrate\Plugin\MigrationPluginManagerInterface; use Drupal\migrate_drupal_ui\Batch\MigrateUpgradeImportBatch; use Drupal\migrate_drupal\MigrationConfigurationTrait; @@ -713,6 +715,20 @@ class MigrateUpgradeForm extends ConfirmFormBase { protected $pluginManager; /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The ID conflict auditor. + * + * @var \Drupal\migrate\MigrateIdAuditor $idAuditor + */ + protected $idAuditor; + + /** * Constructs the MigrateUpgradeForm. * * @param \Drupal\Core\State\StateInterface $state @@ -723,12 +739,18 @@ class MigrateUpgradeForm extends ConfirmFormBase { * The renderer service. * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $plugin_manager * The migration plugin manager. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager + * The entity type manager. + * @param \Drupal\migrate\MigrateIdAuditor $idAuditor + * The ID conflict auditor. */ - public function __construct(StateInterface $state, DateFormatterInterface $date_formatter, RendererInterface $renderer, MigrationPluginManagerInterface $plugin_manager) { + public function __construct(StateInterface $state, DateFormatterInterface $date_formatter, RendererInterface $renderer, MigrationPluginManagerInterface $plugin_manager, EntityTypeManagerInterface $entityTypeManager, MigrateIdAuditor $idAuditor) { $this->state = $state; $this->dateFormatter = $date_formatter; $this->renderer = $renderer; $this->pluginManager = $plugin_manager; + $this->entityTypeManager = $entityTypeManager; + $this->idAuditor = $idAuditor; } /** @@ -739,7 +761,9 @@ public static function create(ContainerInterface $container) { $container->get('state'), $container->get('date.formatter'), $container->get('renderer'), - $container->get('plugin.manager.migration') + $container->get('plugin.manager.migration'), + $container->get('entity_type.manager'), + $container->get('migrate.id_auditor') ); } @@ -762,6 +786,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); @@ -1082,6 +1109,64 @@ 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! + $migrationIds = array_keys($form_state->get('migrations')); + $migrations = $this->pluginManager->createInstances($migrationIds); + $typeIds = $this->idAuditor->auditIds($migrations); + if (empty($typeIds)) { + $form_state->set('step', 'confirm'); + return $this->buildForm($form, $form_state); + } + + $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['warning'] = [ + '#type' => 'markup', + '#markup' => '
' . $this->t('Upgrades work on brand new sites, or sites with previously upgraded data. However, it looks like you have added other entities to your site, perhaps manually. These new entities may be overwritten if you run this upgrade.') . '
', + ]; + + $items = []; + sort($typeIds); + foreach ($typeIds as $typeId) { + $items[] = $this->entityTypeManager->getDefinition($typeId)->getLabel(); + } + $form['type_list'] = [ + '#title' => $this->t('These entities are of the following types:'), + '#theme' => 'item_list', + '#items' => $items, + ]; + + 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/shortcut/migration_templates/d7_shortcut.yml b/core/modules/shortcut/migration_templates/d7_shortcut.yml index dac9354..371e1d8 100644 --- a/core/modules/shortcut/migration_templates/d7_shortcut.yml +++ b/core/modules/shortcut/migration_templates/d7_shortcut.yml @@ -2,6 +2,7 @@ id: d7_shortcut label: Shortcut links migration_tags: - Drupal 7 +preserves_ids: false source: plugin: d7_shortcut constants: diff --git a/core/modules/user/src/Plugin/migrate/destination/EntityUser.php b/core/modules/user/src/Plugin/migrate/destination/EntityUser.php index 7c2c81a..f0206e5 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} + */ + protected function highestDestinationId() { + $found = parent::highestDestinationId(); + + // Every Drupal site must have at least a single non-anonymous user, and + // it's normal for upgrade migrations to overwrite this user. + if ($found == 1) { + return 0; + } + return $found; + } + }