Unverified Commit f2326154 authored by Alex Pott's avatar Alex Pott
Browse files

fix: #3581434 Entity serialization issue in Layout Builder due to content moderation

By: matthijs
By: webflo
By: tstoeckler
By: alexpott
(cherry picked from commit 117fee74)
parent 841cd847
Loading
Loading
Loading
Loading
Loading
+5 −2
Original line number Diff line number Diff line
@@ -1279,11 +1279,14 @@ public function __clone() {
    // Ensure that the following properties are actually cloned by
    // overwriting the original references with ones pointing to copies of
    // them: enforceIsNew, newRevision, loadedRevisionId, fields, entityKeys,
    // translatableEntityKeys, values, isDefaultRevision and
    // enforceRevisionTranslationAffected.
    // translatableEntityKeys, values, isDefaultRevision,
    // enforceRevisionTranslationAffected and originalEntity.
    $enforce_is_new = $this->enforceIsNew;
    $this->enforceIsNew = &$enforce_is_new;

    $originalEntity = $this->originalEntity;
    $this->originalEntity = &$originalEntity;

    $new_revision = $this->newRevision;
    $this->newRevision = &$new_revision;

+110 −4
Original line number Diff line number Diff line
@@ -4,6 +4,8 @@

namespace Drupal\KernelTests\Core\Entity;

use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\RevisionableStorageInterface;
use Drupal\entity_test\Entity\EntityTestMul;
use Drupal\entity_test\Entity\EntityTestMulRev;
use Drupal\language\Entity\ConfigurableLanguage;
@@ -320,13 +322,12 @@ public function testEntityPropertiesModifications(): void {
    ];

    foreach ($properties as $property) {
      // The original property is typed and can not be set to a string.
      if ($property->getName() === 'originalEntity') {
      $property_name = $property->getName();
      // @see \Drupal\KernelTests\Core\Entity\ContentEntityCloneTest::testEntityOriginal()
      if ($property_name === 'originalEntity') {
        continue;
      }

      $property_name = $property->getName();

      // Modify each entity property on the clone and assert that the change is
      // not propagated to the original entity.
      $property->setValue($entity, 'default-value');
@@ -367,4 +368,109 @@ public function testEntityPropertiesModifications(): void {
    }
  }

  /**
   * Tests the entity original property when cloning entities.
   */
  public function testEntityOriginal(): void {
    // Make sure entity bundles are translatable.
    \Drupal::state()->set('entity_test.translation', TRUE);
    $bundleInfo = \Drupal::service('entity_type.bundle.info');
    $this->assertInstanceOf(EntityTypeBundleInfoInterface::class, $bundleInfo);
    $bundleInfo->clearCachedBundles();

    $storage = \Drupal::entityTypeManager()->getStorage('entity_test_mulrev');
    assert($storage instanceof RevisionableStorageInterface);

    // Create a test entity with a translation. The translation will be created
    // by cloning the original entity but the originalEntity property will be
    // synchronized.
    $entity = $storage->create([
      'name' => 'original',
      'language' => 'en',
    ]);
    $this->assertInstanceOf(EntityTestMulRev::class, $entity);
    $this->assertTrue($entity->isTranslatable());

    $this->assertNull($entity->getOriginal());
    $translationLangcode = 'de';
    $translation = $entity->addTranslation($translationLangcode);
    $this->assertInstanceOf(EntityTestMulRev::class, $translation);
    // We could assert here that the original of the entity and the translation
    // is null before and after the save, to avoid false positives below. This,
    // however would lead to a PHPStan error below, due to a bug in PHPStan.
    /* @see https://github.com/phpstan/phpstan/issues/13416 */
    $entity->save();

    // Clone the entity and the translation manually, and make sure that while
    // the entity's and the translation's original are synchronized, the clones
    // are not affected by this.
    $entityClone = clone $entity;
    $entityCloneTranslation = $entityClone->getTranslation($translationLangcode);
    $translationClone = clone $translation;
    $entityOriginal = EntityTestMulRev::create();
    $entity->setOriginal($entityOriginal);
    $this->assertSame($entityOriginal, $entity->getOriginal());
    $this->assertSame($entityOriginal, $translation->getOriginal());
    $this->assertNull($entityClone->getOriginal());
    $this->assertNull($translationClone->getOriginal());

    $entityCloneOriginal = EntityTestMulRev::create();
    $this->assertNotSame($entityOriginal, $entityCloneOriginal);
    $entityCloneTranslation->setOriginal($entityCloneOriginal);
    $this->assertSame($entityOriginal, $entity->getOriginal());
    $this->assertSame($entityOriginal, $translation->getOriginal());
    $this->assertSame($entityCloneOriginal, $entityClone->getOriginal());
    $this->assertSame($entityCloneOriginal, $entityCloneTranslation->getOriginal());
    $this->assertNull($translationClone->getOriginal());

    // Unset the original property.
    $translation->setOriginal(NULL);
    $this->assertNull($entity->getOriginal());
    $this->assertNull($translation->getOriginal());

    // Create two draft revisions of the entity to trigger the merged revision original
    // behavior.
    $firstDraft = $storage->createRevision($entity, default: FALSE);
    $firstDraftClone = clone $firstDraft;
    $this->assertNull($entity->getOriginal());
    $this->assertNull($firstDraft->getOriginal());
    $this->assertNull($firstDraftClone->getOriginal());

    $secondDraft = $storage->createRevision($firstDraft, default: FALSE);
    $this->assertNull($entity->getOriginal());
    $this->assertNull($firstDraft->getOriginal());
    $this->assertNull($firstDraftClone->getOriginal());
    $secondDraftOriginal = $secondDraft->getOriginal();
    $this->assertInstanceOf(EntityTestMulRev::class, $secondDraftOriginal);
    $this->assertNotSame($entityOriginal, $secondDraftOriginal);
    $this->assertNotSame($entityCloneOriginal, $secondDraftOriginal);
    $this->assertNull($secondDraftOriginal->getOriginal());

    // Make sure that the original is still synchronized across translations.
    $secondDraftTranslation = $secondDraft->getTranslation($translationLangcode);
    $this->assertInstanceOf(EntityTestMulRev::class, $secondDraftTranslation);
    $this->assertSame($secondDraftOriginal, $secondDraftTranslation->getOriginal());

    // Test that the synchronization still does not affect clones.
    $secondDraftClone = clone $secondDraft;
    $secondDraftTranslationClone = clone $secondDraftTranslation;
    $secondDraftCloneOriginal = EntityTestMulRev::create();
    $secondDraftClone->setOriginal($secondDraftCloneOriginal);
    $this->assertSame($secondDraftCloneOriginal, $secondDraftClone->getOriginal());
    $secondDraftTranslationClone->setOriginal(NULL);
    $this->assertNull($secondDraftTranslationClone->getOriginal());
    $this->assertSame($secondDraftOriginal, $secondDraft->getOriginal());
    $this->assertSame($secondDraftOriginal, $secondDraftTranslation->getOriginal());

    // Create a third draft revision to trigger the merged revision original
    // again, but this time starting with an entity that already has an original
    // entity set.
    $thirdDraft = $storage->createRevision($secondDraft, default: FALSE);
    $this->assertNull($firstDraft->getOriginal());
    $this->assertSame($secondDraftOriginal, $secondDraft->getOriginal());
    $thirdDraftOriginal = $thirdDraft->getOriginal();
    $this->assertInstanceOf(EntityTestMulRev::class, $thirdDraftOriginal);
    $this->assertNotSame($secondDraftOriginal, $thirdDraftOriginal);
  }

}