diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php index e148f4cc12..0a7a3c1609 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php +++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php @@ -20,15 +20,20 @@ public function validate($entity, Constraint $constraint) { $saved_entity = \Drupal::entityManager()->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id()); // A change to any other translation must add a violation to the current // translation because there might be untranslatable shared fields. - // We have to check first only the current translation as if the entity - // has been saved with an altered non-translatable field meanwhile and - // the previous timestamp was set only on the translation being edited - // then the other translations will still have a newer timestamp and - // the validator will not fail but it should. This might happen if the - // form is not yet cached when being submitted, because on submit the - // entity will be loaded from the storage and the previous changed - // timestamp of the currently edited translation will be set on it in - // order to make this constraint prevent saving. + // + // In the entity form API we put the changed timestamp of the entity in + // a form hidden value and on form submit we set it on the entity which + // will be loaded from the storage if the form is not yet cached. + // Setting the initial changed timestamp on the entity loaded from the + // storage is used as a prevention from saving a form built with a + // previous version of the entity and thus reverting changes by other + // users. However the entity might have been saved in the background in + // a way that has updated the changed timestamps of all entity + // translations and simply comparing the latest changed timestamps + // across all translations is not a sufficient check in this case. + // Instead we have to compare directly the changed timestamps of the + // current entity translation first and after that the changed + // timestamps across all translations. if ($saved_entity) { $langcode = $entity->language()->getId(); if (($saved_entity->hasTranslation($langcode) && ($saved_entity->getTranslation($langcode)->getChangedTime() > $entity->getChangedTime())) || diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityValidationTest.php index 2e4f7bb9b5..5289942b1c 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityValidationTest.php @@ -228,12 +228,17 @@ public function testEntityChangedConstraintOnFormConcurrentEditing() { $this->assertNotEquals($entity->getChangedTime(), $translation->getChangedTime()); - // Simulate form concurrent editing by saving the entity with an altered - // non-translatable field and afterwards setting the previous timestamp of - // an entity translation on the updated entity object and validating it as - // this is exactly what happens in the form API where we put the changed - // time of the entity used to build the form in a hidden value and then set - // it on the entity that is being loaded from the storage. + // Simulate concurrent form editing by saving the entity with an altered + // non-translatable field in order for the changed timestamp to be updated + // across all entity translations. Afterwards we simulate form submitting of + // an uncached form by setting the previous timestamp of an entity + // translation on the saved entity object. This is what happens in the + // entity form API where we put the changed timestamp of the entity in a + // form hidden value and then set it on the entity which on form submit is + // loaded from the storage. Setting the initial changed timestamp on the + // entity loaded from the storage is used as a prevention from saving a form + // built with a previous version of the entity and thus reverting changes by + // other users. $original_entity_time = $entity->getChangedTime(); $entity->set('not_translatable', $this->randomString()); $entity->save();