diff --git a/core/lib/Drupal/Core/Config/ConfigUUIDException.php b/core/lib/Drupal/Core/Config/ConfigUUIDException.php new file mode 100644 index 0000000..72087f0 --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigUUIDException.php @@ -0,0 +1,13 @@ +entityType) + ->condition('uuid', $this->uuid) + ->execute(); + $matched_entity = reset($matching_entities); + if (!empty($matched_entity) && $matched_entity != $this->id) { + throw new ConfigUUIDException(format_string( + 'Attempt to save a configuration object %s with UUID %s when this UUID is already used for %s', + array( + $this->id, + $this->uuid, + $matched_entity, + ) + )); + } + + if (!$this->isNew()) { + // Ensure that the UUID cannot be changed for an existing entity. + if ($original->uuid != $this->uuid) { + throw new ConfigUUIDException(format_string( + 'Attempt to save a configuration entity %s with UUID %s when this entity already exists with UUID %s', + array( + $this->id, + $this->uuid, + $original->uuid, + ) + )); + } + } + + } + } diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldUUIDConflictTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldUUIDConflictTest.php new file mode 100644 index 0000000..239bbe1 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Tests/FieldUUIDConflictTest.php @@ -0,0 +1,144 @@ + 'UUID conflict', + 'description' => 'Tests staging and importing fields and instances with IDs and UUIDs that do not match existing config.', + 'group' => 'Field API', + ); + } + + public function setUp() { + parent::setUp(); + + // Import the default config. + $this->installConfig(array('field_test_config')); + + // These are the types used in field_test_config's default config. + $this->entity_type = 'test_entity'; + $this->bundle = 'test_bundle'; + $this->field_id = 'field_test_import'; + $this->instance_id = "{$this->entity_type}.{$this->bundle}.{$this->field_id}"; + // Load the original field and instance entities. + $this->original_field = entity_load('field_entity', $this->field_id); + $this->original_instance = entity_load('field_instance', $this->instance_id); + } + + /** + * Tests importing fields and instances with changed IDs or UUIDs. + */ + function testUUIDConflict() { + // Stage and attempt to import a changed field ID. + $this->stageIDChange($this->field_id, 'field.field'); + $this->assertNoImport(); + + // Stage and attempt to import a changed instance ID. + $this->stageIDChange($this->instance_id, 'field.instance'); + $this->assertNoImport(); + + // Stage and attempt to import both changed field and instance IDs. + $this->stageIDChange($this->field_id, 'field.field'); + $this->stageIDChange($this->instance_id, 'field.instance'); + $this->assertNoImport(); + + // Stage and attempt to import a changed field UUID. + $this->stageUUIDChange($this->field_id, 'field.field'); + $this->assertNoImport(); + + // Stage and attempt to import a changed instance UUID. + $this->stageUUIDChange($this->instance_id, 'field.instance'); + $this->assertNoImport(); + + // Stage and attempt to import both changed field and instance UUIDs. + $this->stageUUIDChange($this->field_id, 'field.field'); + $this->stageUUIDChange($this->instance_id, 'field.instance'); + $this->assertNoImport(); + } + + /** + * Creates and stages a copy of a configuration object with a different UUID. + */ + public function stageUUIDChange($id, $prefix) { + // Create a copy of the object with the same ID but a different UUID. + $active = $this->container->get('config.storage'); + $config = $active->read("$prefix.$id"); + $uuid = new Uuid(); + $new_uuid = $uuid->generate(); + $config['uuid'] = $new_uuid; + + // Clear any previously staged config and save a file in the staging + // directory. + $staging = $this->container->get('config.storage.staging'); + $staging->deleteAll(); + $staging->write("$prefix.$id", $config); + } + + /** + * Creates and stages a copy of a configuration object with a different ID. + */ + public function stageIDChange($id, $prefix) { + // Create a copy of the object with the same UUID but a different ID. + $active = $this->container->get('config.storage'); + $config = $active->read("$prefix.$id"); + $new_id = strtolower($this->randomName()); + $config['id'] = $new_id; + + // Clear any previously staged config and save a file in the staging + // directory. + $staging = $this->container->get('config.storage.staging'); + $staging->deleteAll(); + $staging->write("$prefix.$new_id", $config); + + // Also add the item to the manifest so that it is detected as new. + // @todo A manifest change should not be needed for new objects, only + // deleted. + $manifest = $active->read("manifest.$prefix"); + $manifest[$new_id] = array('name' => "$prefix.$new_id"); + $staging->write("manifest.$prefix", $manifest); + } + + /** + * Asserts that an exception is thrown and the field data is not corrupted. + */ + public function assertNoImport() { + // Import the content of the staging directory. + try { + config_import(); + $this->fail('Exception thrown when attempting to import a field definition with a UUID that does not match the existing UUID.'); + } + catch (ConfigUUIDException $e) { + $this->pass(format_string('Exception thrown when attempting to import a field definition with an ID/UUID combination that does not match existing data: %e.', array('%e' => $e))); + } + + // Ensure that the field and instance were not corrupted. + $field = entity_load('field_entity', $this->field_id); + $instance = entity_load('field_instance', $this->instance_id); + $this->assertEqual($field, $this->original_field); + $this->assertEqual($instance, $this->original_instance); + } + +}