Pull merge.
[yaffs-website] / web / core / lib / Drupal / Core / Entity / ContentEntityBase.php
index c46126e3633256e75f0e61bf7cac7e40a36ab741..2bf1372b61953b37b98ac6e985ba64dca495be9f 100644 (file)
@@ -2,10 +2,9 @@
 
 namespace Drupal\Core\Entity;
 
-use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Entity\Plugin\DataType\EntityReference;
 use Drupal\Core\Field\BaseFieldDefinition;
-use Drupal\Core\Field\ChangedFieldItemList;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Session\AccountInterface;
@@ -20,6 +19,10 @@ use Drupal\Core\TypedData\TypedDataInterface;
  */
 abstract class ContentEntityBase extends Entity implements \IteratorAggregate, ContentEntityInterface, TranslationStatusInterface {
 
+  use EntityChangesDetectionTrait {
+    getFieldsToSkipFromTranslationChangesCheck as traitGetFieldsToSkipFromTranslationChangesCheck;
+  }
+
   /**
    * The plain data values of the contained fields.
    *
@@ -156,6 +159,29 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
    */
   protected $loadedRevisionId;
 
+  /**
+   * The revision translation affected entity key.
+   *
+   * @var string
+   */
+  protected $revisionTranslationAffectedKey;
+
+  /**
+   * Whether the revision translation affected flag has been enforced.
+   *
+   * An array, keyed by the translation language code.
+   *
+   * @var bool[]
+   */
+  protected $enforceRevisionTranslationAffected = [];
+
+  /**
+   * Local cache for fields to skip from the checking for translation changes.
+   *
+   * @var array
+   */
+  protected static $fieldsToSkipFromTranslationChangesCheck = [];
+
   /**
    * {@inheritdoc}
    */
@@ -164,6 +190,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
     $this->entityKeys['bundle'] = $bundle ? $bundle : $this->entityTypeId;
     $this->langcodeKey = $this->getEntityType()->getKey('langcode');
     $this->defaultLangcodeKey = $this->getEntityType()->getKey('default_langcode');
+    $this->revisionTranslationAffectedKey = $this->getEntityType()->getKey('revision_translation_affected');
 
     foreach ($values as $key => $value) {
       // If the key matches an existing property set the value to the property
@@ -269,15 +296,11 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
       // When saving a new revision, set any existing revision ID to NULL so as
       // to ensure that a new revision will actually be created.
       $this->set($this->getEntityType()->getKey('revision'), NULL);
-
-      // Make sure that the flag tracking which translations are affected by the
-      // current revision is reset.
-      foreach ($this->translations as $langcode => $data) {
-        // But skip removed translations.
-        if ($this->hasTranslation($langcode)) {
-          $this->getTranslation($langcode)->setRevisionTranslationAffected(NULL);
-        }
-      }
+    }
+    elseif (!$value && $this->newRevision) {
+      // If ::setNewRevision(FALSE) is called after ::setNewRevision(TRUE) we
+      // have to restore the loaded revision ID.
+      $this->set($this->getEntityType()->getKey('revision'), $this->getLoadedRevisionId());
     }
 
     $this->newRevision = $value;
@@ -318,25 +341,73 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
     return $this->isNew() || $return;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function wasDefaultRevision() {
+    /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
+    $entity_type = $this->getEntityType();
+    if (!$entity_type->isRevisionable()) {
+      return TRUE;
+    }
+
+    $revision_default_key = $entity_type->getRevisionMetadataKey('revision_default');
+    $value = $this->isNew() || $this->get($revision_default_key)->value;
+    return $value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isLatestRevision() {
+    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+    $storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
+
+    return $this->getLoadedRevisionId() == $storage->getLatestRevisionId($this->id());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isLatestTranslationAffectedRevision() {
+    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+    $storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
+
+    return $this->getLoadedRevisionId() == $storage->getLatestTranslationAffectedRevisionId($this->id(), $this->language()->getId());
+  }
+
   /**
    * {@inheritdoc}
    */
   public function isRevisionTranslationAffected() {
-    $field_name = 'revision_translation_affected';
-    return $this->hasField($field_name) ? $this->get($field_name)->value : TRUE;
+    return $this->hasField($this->revisionTranslationAffectedKey) ? $this->get($this->revisionTranslationAffectedKey)->value : TRUE;
   }
 
   /**
    * {@inheritdoc}
    */
   public function setRevisionTranslationAffected($affected) {
-    $field_name = 'revision_translation_affected';
-    if ($this->hasField($field_name)) {
-      $this->set($field_name, $affected);
+    if ($this->hasField($this->revisionTranslationAffectedKey)) {
+      $this->set($this->revisionTranslationAffectedKey, $affected);
     }
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isRevisionTranslationAffectedEnforced() {
+    return !empty($this->enforceRevisionTranslationAffected[$this->activeLangcode]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRevisionTranslationAffectedEnforced($enforced) {
+    $this->enforceRevisionTranslationAffected[$this->activeLangcode] = $enforced;
+    return $this;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -355,8 +426,8 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
    * {@inheritdoc}
    */
   public function isTranslatable() {
-    // Check that the bundle is translatable, the entity has a language defined
-    // and if we have more than one language on the site.
+    // Check the bundle is translatable, the entity has a language defined, and
+    // the site has more than one language.
     $bundles = $this->entityManager()->getBundleInfo($this->entityTypeId);
     return !empty($bundles[$this->bundle()]['translatable']) && !$this->getUntranslated()->language()->isLocked() && $this->languageManager()->isMultilingual();
   }
@@ -401,6 +472,12 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
       }
     }
     $this->translations = array_diff_key($this->translations, $removed);
+
+    // Reset the new revision flag.
+    $this->newRevision = FALSE;
+
+    // Reset the enforcement of the revision translation affected flag.
+    $this->enforceRevisionTranslationAffected = [];
   }
 
   /**
@@ -701,11 +778,11 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
    * {@inheritdoc}
    */
   public function onChange($name) {
-    // Check if the changed name is the value of an entity key and if the value
-    // of that is currently cached, if so, reset it. Exclude the bundle from
-    // that check, as it ready only and must not change, unsetting it could
+    // Check if the changed name is the value of any entity keys and if any of
+    // those values are currently cached, if so, reset it. Exclude the bundle
+    // from that check, as it ready only and must not change, unsetting it could
     // lead to recursions.
-    if ($key = array_search($name, $this->getEntityType()->getKeys())) {
+    foreach (array_keys($this->getEntityType()->getKeys(), $name, TRUE) as $key) {
       if ($key != 'bundle') {
         if (isset($this->entityKeys[$key])) {
           unset($this->entityKeys[$key]);
@@ -713,6 +790,12 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
         elseif (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) {
           unset($this->translatableEntityKeys[$key][$this->activeLangcode]);
         }
+        // If the revision identifier field is being populated with the original
+        // value, we need to make sure the "new revision" flag is reset
+        // accordingly.
+        if ($key === 'revision' && $this->getRevisionId() == $this->getLoadedRevisionId() && !$this->isNew()) {
+          $this->newRevision = FALSE;
+        }
       }
     }
 
@@ -722,7 +805,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
           // Update the default internal language cache.
           $this->setDefaultLangcode();
           if (isset($this->translations[$this->defaultLangcode])) {
-            $message = SafeMarkup::format('A translation already exists for the specified language (@langcode).', ['@langcode' => $this->defaultLangcode]);
+            $message = new FormattableMarkup('A translation already exists for the specified language (@langcode).', ['@langcode' => $this->defaultLangcode]);
             throw new \InvalidArgumentException($message);
           }
           $this->updateFieldLangcodes($this->defaultLangcode);
@@ -733,7 +816,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
           $items = $this->get($this->langcodeKey);
           if ($items->value != $this->activeLangcode) {
             $items->setValue($this->activeLangcode, FALSE);
-            $message = SafeMarkup::format('The translation language cannot be changed (@langcode).', ['@langcode' => $this->activeLangcode]);
+            $message = new FormattableMarkup('The translation language cannot be changed (@langcode).', ['@langcode' => $this->activeLangcode]);
             throw new \LogicException($message);
           }
         }
@@ -744,10 +827,16 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
         //   read-only. See https://www.drupal.org/node/2443991.
         if (isset($this->values[$this->defaultLangcodeKey]) && $this->get($this->defaultLangcodeKey)->value != $this->isDefaultTranslation()) {
           $this->get($this->defaultLangcodeKey)->setValue($this->isDefaultTranslation(), FALSE);
-          $message = SafeMarkup::format('The default translation flag cannot be changed (@langcode).', ['@langcode' => $this->activeLangcode]);
+          $message = new FormattableMarkup('The default translation flag cannot be changed (@langcode).', ['@langcode' => $this->activeLangcode]);
           throw new \LogicException($message);
         }
         break;
+
+      case $this->revisionTranslationAffectedKey:
+        // If the revision translation affected flag is being set then enforce
+        // its value.
+        $this->setRevisionTranslationAffectedEnforced(TRUE);
+        break;
     }
   }
 
@@ -763,8 +852,8 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
 
     // Populate entity translation object cache so it will be available for all
     // translation objects.
-    if ($langcode == $this->activeLangcode) {
-      $this->translations[$langcode]['entity'] = $this;
+    if (!isset($this->translations[$this->activeLangcode]['entity'])) {
+      $this->translations[$this->activeLangcode]['entity'] = $this;
     }
 
     // If we already have a translation object for the specified language we can
@@ -831,6 +920,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
     $translation->typedData = NULL;
     $translation->loadedRevisionId = &$this->loadedRevisionId;
     $translation->isDefaultRevision = &$this->isDefaultRevision;
+    $translation->enforceRevisionTranslationAffected = &$this->enforceRevisionTranslationAffected;
 
     return $translation;
   }
@@ -916,7 +1006,9 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
    * {@inheritdoc}
    */
   public function getTranslationLanguages($include_default = TRUE) {
-    $translations = array_filter($this->translations, function($translation) { return $translation['status']; });
+    $translations = array_filter($this->translations, function ($translation) {
+      return $translation['status'];
+    });
     unset($translations[LanguageInterface::LANGCODE_DEFAULT]);
 
     if ($include_default) {
@@ -1041,7 +1133,9 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
 
     $duplicate = clone $this;
     $entity_type = $this->getEntityType();
-    $duplicate->{$entity_type->getKey('id')}->value = NULL;
+    if ($entity_type->hasKey('id')) {
+      $duplicate->{$entity_type->getKey('id')}->value = NULL;
+    }
     $duplicate->enforceIsNew();
 
     // Check if the entity type supports UUIDs and generate a new one if so.
@@ -1096,7 +1190,8 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
     // 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 and isDefaultRevision.
+    // translatableEntityKeys, values, isDefaultRevision and
+    // enforceRevisionTranslationAffected.
     $enforce_is_new = $this->enforceIsNew;
     $this->enforceIsNew = &$enforce_is_new;
 
@@ -1121,6 +1216,9 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
     $default_revision = $this->isDefaultRevision;
     $this->isDefaultRevision = &$default_revision;
 
+    $is_revision_translation_affected_enforced = $this->enforceRevisionTranslationAffected;
+    $this->enforceRevisionTranslationAffected = &$is_revision_translation_affected_enforced;
+
     foreach ($this->fields as $name => $fields_by_langcode) {
       $this->fields[$name] = [];
       // Untranslatable fields may have multiple references for the same field
@@ -1285,22 +1383,11 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
    *   An array of field names.
    */
   protected function getFieldsToSkipFromTranslationChangesCheck() {
-    // A list of known revision metadata fields which should be skipped from
-    // the comparision.
-    // @todo Replace the hard coded list of revision metadata fields with the
-    // solution from https://www.drupal.org/node/2615016.
-    $fields = [
-      $this->getEntityType()->getKey('revision'),
-      'revision_translation_affected',
-      'revision_uid',
-      'revision_user',
-      'revision_timestamp',
-      'revision_created',
-      'revision_log',
-      'revision_log_message',
-    ];
-
-    return $fields;
+    $bundle = $this->bundle();
+    if (!isset(static::$fieldsToSkipFromTranslationChangesCheck[$this->entityTypeId][$bundle])) {
+      static::$fieldsToSkipFromTranslationChangesCheck[$this->entityTypeId][$bundle] = $this->traitGetFieldsToSkipFromTranslationChangesCheck($this);
+    }
+    return static::$fieldsToSkipFromTranslationChangesCheck[$this->entityTypeId][$bundle];
   }
 
   /**
@@ -1336,32 +1423,40 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
     // possible or be meaningless.
     /** @var \Drupal\Core\Entity\ContentEntityBase $translation */
     $translation = $original->getTranslation($this->activeLangcode);
+    $langcode = $this->language()->getId();
 
     // The list of fields to skip from the comparision.
     $skip_fields = $this->getFieldsToSkipFromTranslationChangesCheck();
 
+    // We also check untranslatable fields, so that a change to those will mark
+    // all translations as affected, unless they are configured to only affect
+    // the default translation.
+    $skip_untranslatable_fields = !$this->isDefaultTranslation() && $this->isDefaultTranslationAffectedOnly();
+
     foreach ($this->getFieldDefinitions() as $field_name => $definition) {
       // @todo Avoid special-casing the following fields. See
       //    https://www.drupal.org/node/2329253.
-      if (in_array($field_name, $skip_fields, TRUE)) {
+      if (in_array($field_name, $skip_fields, TRUE) || ($skip_untranslatable_fields && !$definition->isTranslatable())) {
         continue;
       }
-      $field = $this->get($field_name);
-      // When saving entities in the user interface, the changed timestamp is
-      // automatically incremented by ContentEntityForm::submitForm() even if
-      // nothing was actually changed. Thus, the changed time needs to be
-      // ignored when determining whether there are any actual changes in the
-      // entity.
-      if (!($field instanceof ChangedFieldItemList) && !$definition->isComputed()) {
-        $items = $field->filterEmptyItems();
-        $original_items = $translation->get($field_name)->filterEmptyItems();
-        if (!$items->equals($original_items)) {
-          return TRUE;
-        }
+      $items = $this->get($field_name)->filterEmptyItems();
+      $original_items = $translation->get($field_name)->filterEmptyItems();
+      if ($items->hasAffectingChanges($original_items, $langcode)) {
+        return TRUE;
       }
     }
 
     return FALSE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isDefaultTranslationAffectedOnly() {
+    $bundle_name = $this->bundle();
+    $bundle_info = \Drupal::service('entity_type.bundle.info')
+      ->getBundleInfo($this->getEntityTypeId());
+    return !empty($bundle_info[$bundle_name]['untranslatable_fields.default_translation_affected']);
+  }
+
 }