Commit 0c20200d authored by catch's avatar catch
Browse files

Issue #2837022 by hchonov, xjm, vlad.dancer, plach, matsbla, Gábor Hojtsy:...

Issue #2837022 by hchonov, xjm, vlad.dancer, plach, matsbla, Gábor Hojtsy: Concurrently editing two translations of a node may result in data loss for non-translatable fields
parent 4cd7b495
......@@ -37,6 +37,11 @@ public function setChangedTime($timestamp);
* Gets the timestamp of the last entity change across all translations.
* This method will return the highest timestamp across all translations. To
* check that no translation is older than in another version of the entity
* (e.g. to avoid overwriting newer translations with old data), compare each
* translation to the other version individually.
* @return int
* The timestamp of the last entity save operation across all
* translations.
......@@ -18,10 +18,23 @@ public function validate($entity, Constraint $constraint) {
/** @var \Drupal\Core\Entity\EntityInterface $entity */
if (!$entity->isNew()) {
$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.
if ($saved_entity && $saved_entity->getChangedTimeAcrossTranslations() > $entity->getChangedTimeAcrossTranslations()) {
// Ensure that all the entity translations are the same as or newer
// than their current version in the storage in order to avoid
// reverting other changes. In fact the entity object that is being
// saved might contain an older entity translation when different
// translations are being concurrently edited.
if ($saved_entity) {
$common_translation_languages = array_intersect_key($entity->getTranslationLanguages(), $saved_entity->getTranslationLanguages());
foreach (array_keys($common_translation_languages) as $langcode) {
// Merely comparing the latest changed timestamps across all
// translations is not sufficient since other translations may have
// been edited and saved in the meanwhile. Therefore, compare the
// changed timestamps of each entity translation individually.
if ($saved_entity->getTranslation($langcode)->getChangedTime() > $entity->getTranslation($langcode)->getChangedTime()) {
......@@ -3,6 +3,7 @@
namespace Drupal\KernelTests\Core\Entity;
use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase;
use Drupal\language\Entity\ConfigurableLanguage;
* Tests the Entity Validation API.
......@@ -16,7 +17,7 @@ class EntityValidationTest extends EntityKernelTestBase {
* @var array
public static $modules = ['filter', 'text'];
public static $modules = ['filter', 'text', 'language'];
* @var string
......@@ -39,6 +40,10 @@ class EntityValidationTest extends EntityKernelTestBase {
protected function setUp() {
// Enable an additional language.
// Create the test field.
......@@ -200,4 +205,49 @@ public function testCompositeConstraintValidation() {
$this->assertEqual($constraint->coversFields(), ['name', 'type'], 'Information about covered fields can be retrieved.');
* Tests the EntityChangedConstraintValidator with multiple translations.
public function testEntityChangedConstraintOnConcurrentMultilingualEditing() {
$storage = \Drupal::entityTypeManager()
// Create a test entity.
$entity = $this->createTestEntity('entity_test_mulrev_changed');
$entity->setChangedTime($entity->getChangedTime() - 1);
$violations = $entity->validate();
$this->assertEquals(1, $violations->count());
$this->assertEqual($violations[0]->getMessage(), 'The content has either been modified by another user, or you have already submitted modifications. As a result, your changes cannot be saved.');
$entity = $storage->loadUnchanged($entity->id());
$translation = $entity->addTranslation('de');
// Ensure that the new translation has a newer changed timestamp than the
// default translation.
$this->assertGreaterThan($entity->getChangedTime(), $translation->getChangedTime());
// 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.
$original_entity_time = $entity->getChangedTime();
$entity->set('not_translatable', $this->randomString());
// Simulate form submission of an uncached form by setting the previous
// timestamp of an entity translation on the saved entity object. This
// 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 if the form is not yet cached.
// Setting the changed timestamp from the user input 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.
$violations = $entity->validate();
$this->assertEquals(1, $violations->count());
$this->assertEqual($violations[0]->getMessage(), 'The content has either been modified by another user, or you have already submitted modifications. As a result, your changes cannot be saved.');
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment