From 0d06b25026e5bb340c63017ac4942778d0a02f38 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Sun, 26 Jul 2015 16:49:58 +0100
Subject: [PATCH] Issue #2513094 by hchonov, pfrenssen, mkalkbrenner, yched,
 Berdir: ContentEntityBase::getTranslatedField and ContentEntityBase::__clone
 break field reference to parent entity

---
 .../Drupal/Core/Entity/ContentEntityBase.php  | 17 ++++-
 .../Core/Entity/ContentEntityStorageBase.php  |  7 +-
 .../Core/Entity/FieldableEntityInterface.php  | 13 +++-
 ...erenceFieldTranslatedReferenceViewTest.php |  4 +-
 .../Tests/Entity/ContentEntityCloneTest.php   | 70 +++++++++++++++++++
 .../Tests/Entity/EntityTranslationTest.php    | 29 ++++++++
 .../Tests/TaxonomyTranslationTestTrait.php    |  8 ++-
 .../Tests/TermTranslationFieldViewTest.php    |  4 +-
 8 files changed, 142 insertions(+), 10 deletions(-)
 create mode 100644 core/modules/system/src/Tests/Entity/ContentEntityCloneTest.php

diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
index aae28963b119..27d130fd9fb6 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -489,7 +489,7 @@ protected function getTranslatedField($name, $langcode) {
         if (isset($this->values[$name][$langcode])) {
           $value = $this->values[$name][$langcode];
         }
-        $field = \Drupal::service('plugin.manager.field.field_type')->createFieldItemList($this, $name, $value);
+        $field = \Drupal::service('plugin.manager.field.field_type')->createFieldItemList($this->getTranslation($langcode), $name, $value);
         if ($default) {
           // $this->defaultLangcode might not be set if we are initializing the
           // default language code cache, in which case there is no valid
@@ -530,6 +530,19 @@ public function getFields($include_computed = TRUE) {
     return $fields;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getTranslatableFields($include_computed = TRUE) {
+    $fields = [];
+    foreach ($this->getFieldDefinitions() as $name => $definition) {
+      if (($include_computed || !$definition->isComputed()) && $definition->isTranslatable()) {
+        $fields[$name] = $this->get($name);
+      }
+    }
+    return $fields;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -1041,7 +1054,7 @@ public function __clone() {
         }
         foreach ($values as $langcode => $items) {
           $this->fields[$name][$langcode] = clone $items;
-          $this->fields[$name][$langcode]->setContext($name, $this->getTypedData());
+          $this->fields[$name][$langcode]->setContext($name, $this->getTranslation($langcode)->getTypedData());
         }
       }
 
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
index c3ae6a304b3b..9240827f96b7 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -405,7 +405,12 @@ protected function invokeFieldMethod($method, ContentEntityInterface $entity) {
     $args = array_slice(func_get_args(), 2);
     foreach (array_keys($entity->getTranslationLanguages()) as $langcode) {
       $translation = $entity->getTranslation($langcode);
-      foreach ($translation->getFields() as $name => $items) {
+      // For non translatable fields, there is only one field object instance
+      // across all translations and it has as parent entity the entity in the
+      // default entity translation. Therefore field methods on non translatable
+      // fields should be invoked only on the default entity translation.
+      $fields = $translation->isDefaultTranslation() ? $translation->getFields() : $translation->getTranslatableFields();
+      foreach ($fields as $name => $items) {
         // call_user_func_array() is way slower than a direct call so we avoid
         // using it if have no parameters.
         $result[$langcode][$name] = $args ? call_user_func_array([$items, $method], $args) : $items->{$method}();
diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php b/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php
index 2e5cce76fb67..a83548b530fe 100644
--- a/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php
+++ b/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php
@@ -175,7 +175,7 @@ public function get($field_name);
   public function set($field_name, $value, $notify = TRUE);
 
   /**
-   * Gets an array of field item lists.
+   * Gets an array of all field item lists.
    *
    * @param bool $include_computed
    *   If set to TRUE, computed fields are included. Defaults to TRUE.
@@ -185,6 +185,17 @@ public function set($field_name, $value, $notify = TRUE);
    */
   public function getFields($include_computed = TRUE);
 
+  /**
+   * Gets an array of field item lists for translatable fields.
+   *
+   * @param bool $include_computed
+   *   If set to TRUE, computed fields are included. Defaults to TRUE.
+   *
+   * @return \Drupal\Core\Field\FieldItemListInterface[]
+   *   An array of field item lists implementing, keyed by field name.
+   */
+  public function getTranslatableFields($include_computed = TRUE);
+
   /**
    * Reacts to changes to a field.
    *
diff --git a/core/modules/entity_reference/src/Tests/EntityReferenceFieldTranslatedReferenceViewTest.php b/core/modules/entity_reference/src/Tests/EntityReferenceFieldTranslatedReferenceViewTest.php
index 648920b7ece8..f72a4de4fc28 100644
--- a/core/modules/entity_reference/src/Tests/EntityReferenceFieldTranslatedReferenceViewTest.php
+++ b/core/modules/entity_reference/src/Tests/EntityReferenceFieldTranslatedReferenceViewTest.php
@@ -188,7 +188,7 @@ protected function setUpEntityReferenceField() {
       'entity_type' => $this->testEntityTypeName,
       'type' => 'entity_reference',
       'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
-      'translatable' => FALSE,
+      'translatable' => TRUE,
       'settings' => array(
         'allowed_values' => array(
           array(
@@ -292,7 +292,7 @@ protected function createReferrerEntity() {
       'langcode' => $this->baseLangcode,
     ));
     $node->save();
-    $node->addTranslation($this->translateToLangcode, array());
+    $node->addTranslation($this->translateToLangcode, $node->toArray());
     $node->save();
 
     return $node;
diff --git a/core/modules/system/src/Tests/Entity/ContentEntityCloneTest.php b/core/modules/system/src/Tests/Entity/ContentEntityCloneTest.php
new file mode 100644
index 000000000000..77d1603985ef
--- /dev/null
+++ b/core/modules/system/src/Tests/Entity/ContentEntityCloneTest.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Entity\ContentEntityCloneTest.
+ */
+
+namespace Drupal\system\Tests\Entity;
+
+use Drupal\entity_test\Entity\EntityTestMul;
+use Drupal\language\Entity\ConfigurableLanguage;
+
+/**
+ * Tests proper cloning of content entities.
+ *
+ * @group Entity
+ */
+class ContentEntityCloneTest extends EntityUnitTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['language', 'entity_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Enable an additional language.
+    ConfigurableLanguage::createFromLangcode('de')->save();
+
+    $this->installEntitySchema('entity_test_mul');
+  }
+
+  /**
+   * Tests if entity references on fields are still correct after cloning.
+   */
+  public function testFieldEntityReferenceAfterClone() {
+    $user = $this->createUser();
+
+    // Create a test entity.
+    $entity = EntityTestMul::create([
+      'name' => $this->randomString(),
+      'user_id' => $user->id(),
+      'language' => 'en',
+    ]);
+
+    $clone = clone $entity->addTranslation('de');
+
+    $this->assertEqual($entity->getTranslationLanguages(), $clone->getTranslationLanguages(), 'The entity and its clone have the same translation languages.');
+
+    $default_langcode = $entity->getUntranslated()->language()->getId();
+    foreach (array_keys($clone->getTranslationLanguages()) as $langcode) {
+      $translation = $clone->getTranslation($langcode);
+      foreach ($translation->getFields() as $field_name => $field) {
+        if ($field->getFieldDefinition()->isTranslatable()) {
+          $args = ['%field_name' => $field_name, '%langcode' => $langcode];
+          $this->assertEqual($langcode, $field->getEntity()->language()->getId(), format_string('Translatable field %field_name on translation %langcode has correct entity reference in translation %langcode after cloning.', $args));
+        }
+        else {
+          $args = ['%field_name' => $field_name, '%langcode' => $langcode, '%default_langcode' => $default_langcode];
+          $this->assertEqual($default_langcode, $field->getEntity()->language()->getId(), format_string('Non translatable field %field_name on translation %langcode has correct entity reference in the default translation %default_langcode after cloning.', $args));
+        }
+      }
+    }
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Entity/EntityTranslationTest.php b/core/modules/system/src/Tests/Entity/EntityTranslationTest.php
index 88f21c932e1c..21ff39284556 100644
--- a/core/modules/system/src/Tests/Entity/EntityTranslationTest.php
+++ b/core/modules/system/src/Tests/Entity/EntityTranslationTest.php
@@ -769,4 +769,33 @@ function testEntityAdapter() {
     }
   }
 
+  /**
+   * Tests if entity references are correct after adding a new translation.
+   */
+  public function testFieldEntityReference() {
+    $entity_type = 'entity_test_mul';
+    $controller = $this->entityManager->getStorage($entity_type);
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = $controller->create();
+
+    foreach ($this->langcodes as $langcode) {
+      $entity->addTranslation($langcode);
+    }
+
+    $default_langcode = $entity->getUntranslated()->language()->getId();
+    foreach (array_keys($entity->getTranslationLanguages()) as $langcode) {
+      $translation = $entity->getTranslation($langcode);
+      foreach ($translation->getFields() as $field_name => $field) {
+        if ($field->getFieldDefinition()->isTranslatable()) {
+          $args = ['%field_name' => $field_name, '%langcode' => $langcode];
+          $this->assertEqual($langcode, $field->getEntity()->language()->getId(), format_string('Translatable field %field_name on translation %langcode has correct entity reference in translation %langcode.', $args));
+        }
+        else {
+          $args = ['%field_name' => $field_name, '%langcode' => $langcode, '%default_langcode' => $default_langcode];
+          $this->assertEqual($default_langcode, $field->getEntity()->language()->getId(), format_string('Non translatable field %field_name on translation %langcode has correct entity reference in the default translation %default_langcode.', $args));
+        }
+      }
+    }
+  }
+
 }
diff --git a/core/modules/taxonomy/src/Tests/TaxonomyTranslationTestTrait.php b/core/modules/taxonomy/src/Tests/TaxonomyTranslationTestTrait.php
index 61e45bd692c3..4b828cfb2dec 100644
--- a/core/modules/taxonomy/src/Tests/TaxonomyTranslationTestTrait.php
+++ b/core/modules/taxonomy/src/Tests/TaxonomyTranslationTestTrait.php
@@ -78,8 +78,12 @@ protected function enableTranslation() {
 
   /**
    * Adds term reference field for the article content type.
+   *
+   * @param bool $translatable
+   *   (optional) If TRUE, create a translatable term reference field. Defaults
+   *   to FALSE.
    */
-  protected function setUpTermReferenceField() {
+  protected function setUpTermReferenceField($translatable = FALSE) {
     $handler_settings = array(
       'target_bundles' => array(
         $this->vocabulary->id() => $this->vocabulary->id(),
@@ -88,7 +92,7 @@ protected function setUpTermReferenceField() {
     );
     $this->createEntityReferenceField('node', 'article', $this->termFieldName, NULL, 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
     $field_storage = FieldStorageConfig::loadByName('node', $this->termFieldName);
-    $field_storage->setTranslatable(FALSE);
+    $field_storage->setTranslatable($translatable);
     $field_storage->save();
 
     entity_get_form_display('node', 'article', 'default')
diff --git a/core/modules/taxonomy/src/Tests/TermTranslationFieldViewTest.php b/core/modules/taxonomy/src/Tests/TermTranslationFieldViewTest.php
index 9474d11c5e18..e9d2afe27a0b 100644
--- a/core/modules/taxonomy/src/Tests/TermTranslationFieldViewTest.php
+++ b/core/modules/taxonomy/src/Tests/TermTranslationFieldViewTest.php
@@ -50,7 +50,7 @@ protected function setUp() {
     $this->vocabulary = $this->createVocabulary();
     $this->enableTranslation();
     $this->setUpTerm();
-    $this->setUpTermReferenceField();
+    $this->setUpTermReferenceField(TRUE);
     $this->setUpNode();
   }
 
@@ -85,7 +85,7 @@ protected function setUpNode() {
       'langcode' => $this->baseLangcode,
     ));
     $node->save();
-    $node->addTranslation($this->translateToLangcode, array());
+    $node->addTranslation($this->translateToLangcode, $node->toArray());
     $node->save();
     $this->node = $node;
   }
-- 
GitLab