diff --git a/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/Field.php b/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/Field.php index e7e9bd5..84fe6ce 100644 --- a/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/Field.php +++ b/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/Field.php @@ -271,6 +271,23 @@ public function save() { // Clear the derived data about the field. unset($this->schema, $this->storageDetails); + // Ensure this field UUID does not exist with a different ID, regardless + // of whether it's new or updated. + $matching_fields = \Drupal::entityQuery('field_entity') + ->condition('uuid', $this->uuid) + ->execute(); + $matched_field = reset($matching_fields); + if (!empty($matched_field) && $matched_field != $this->id) { + throw new FieldException(format_string( + 'Attempt to create a field @input_id with UUID @input_uuid when this UUID is already used for @field_id', + array( + '@input_id' => $this->id, + '@input_uuid' => $this->uuid, + '@field_id' => $matched_field, + ) + )); + } + if ($this->isNew()) { // Field name cannot be longer than Field::ID_MAX_LENGTH characters. We // use drupal_strlen() because the DB layer assumes that column widths @@ -337,6 +354,17 @@ public function save() { // Otherwise, the field is being updated. else { $original = $storage_controller->loadUnchanged($this->id()); + // Ensure this field ID does not exist with a different UUID. + if ($original->uuid != $this->uuid) { + throw new FieldException(format_string( + 'Attempt to create a field @input_id with UUID @input_uuid when this field already exists with UUID @field_uuid', + array( + '@input_id' => $this->id, + '@input_uuid' => $this->uuid, + '@field_uuid' => $original->uuid, + ) + )); + } // Some updates are always disallowed. if ($this->type != $original->type) { diff --git a/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/FieldInstance.php b/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/FieldInstance.php index 53132ec..61355ae 100644 --- a/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/FieldInstance.php +++ b/core/modules/field/lib/Drupal/field/Plugin/Core/Entity/FieldInstance.php @@ -324,6 +324,23 @@ public function save() { $entity_manager = \Drupal::entityManager(); $instance_controller = $entity_manager->getStorageController($this->entityType); + // Ensure this UUID does not exist for a different field instance, + // regardless of whether it's new or updated. + $matching_instances = \Drupal::entityQuery('field_instance') + ->condition('uuid', $this->uuid) + ->execute(); + $matched_instance = reset($matching_instances); + if (!empty($matched_instance) && $matched_instance != $this->id) { + throw new FieldException(format_string( + 'Attempt to create a field instance @input_id with UUID @input_uuid when this UUID is already used for @instance_id', + array( + '@input_id' => $this->id, + '@input_uuid' => $this->uuid, + '@instance_id' => $matched_instance, + ) + )); + } + if ($this->isNew()) { // Check that the field can be attached to this entity type. if (!empty($this->field->entity_types) && !in_array($this->entity_type, $this->field->entity_types)) { @@ -347,7 +364,19 @@ public function save() { ->getStorageController($this->entityType) ->loadUnchanged($this->getOriginalID()); - // Some updates are always disallowed. + // Ensure this field instance does not exist with a different UUID. + if ($original->uuid != $this->uuid) { + throw new FieldException(format_string( + 'Attempt to create a field instance @input_id with UUID @input_uuid when this field instance already exists with UUID @instance_uuid', + array( + '@input_id' => $this->id, + '@input_uuid' => $this->uuid, + '@instance_uuid' => $original->uuid, + ) + )); + } + + // Some updates are always disallowed. if ($this->entity_type != $original->entity_type) { throw new FieldException("Cannot change an existing instance's entity_type."); } 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..8ff5182 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Tests/FieldUUIDConflictTest.php @@ -0,0 +1,143 @@ + '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 (FieldException $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); + } + +}