Updated to Drupal 8.5. Core Media not yet in use.
[yaffs-website] / web / core / lib / Drupal / Core / Entity / ContentEntityStorageBase.php
index 22d9caf2b833ff9448a8f0b2ee486db6a5ca6f5a..a004ebad8142fabee4c9e29145bc573f5e6a60a9 100644 (file)
@@ -6,6 +6,7 @@ use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\TypedData\TranslationStatusInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -79,6 +80,40 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
     return $entity;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function createWithSampleValues($bundle = FALSE, array $values = []) {
+    // ID and revision should never have sample values generated for them.
+    $forbidden_keys = [
+      $this->entityType->getKey('id'),
+    ];
+    if ($revision_key = $this->entityType->getKey('revision')) {
+      $forbidden_keys[] = $revision_key;
+    }
+    if ($bundle_key = $this->entityType->getKey('bundle')) {
+      if (!$bundle) {
+        throw new EntityStorageException("No entity bundle was specified");
+      }
+      if (!array_key_exists($bundle, $this->entityManager->getBundleInfo($this->entityTypeId))) {
+        throw new EntityStorageException(sprintf("Missing entity bundle. The \"%s\" bundle does not exist", $bundle));
+      }
+      $values[$bundle_key] = $bundle;
+      // Bundle is already set
+      $forbidden_keys[] = $bundle_key;
+    }
+    // Forbid sample generation on any keys whose values were submitted.
+    $forbidden_keys = array_merge($forbidden_keys, array_keys($values));
+    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
+    $entity = $this->create($values);
+    foreach ($entity as $field_name => $value) {
+      if (!in_array($field_name, $forbidden_keys, TRUE)) {
+        $entity->get($field_name)->generateSampleItems();
+      }
+    }
+    return $entity;
+  }
+
   /**
    * Initializes field values.
    *
@@ -114,6 +149,69 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
     $this->invokeHook('field_values_init', $entity);
   }
 
+  /**
+   * Checks whether any entity revision is translated.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\TranslatableInterface $entity
+   *   The entity object to be checked.
+   *
+   * @return bool
+   *   TRUE if the entity has at least one translation in any revision, FALSE
+   *   otherwise.
+   *
+   * @see \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages()
+   * @see \Drupal\Core\Entity\ContentEntityStorageBase::isAnyStoredRevisionTranslated()
+   */
+  protected function isAnyRevisionTranslated(TranslatableInterface $entity) {
+    return $entity->getTranslationLanguages(FALSE) || $this->isAnyStoredRevisionTranslated($entity);
+  }
+
+  /**
+   * Checks whether any stored entity revision is translated.
+   *
+   * A revisionable entity can have translations in a pending revision, hence
+   * the default revision may appear as not translated. This determines whether
+   * the entity has any translation in the storage and thus should be considered
+   * as multilingual.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\TranslatableInterface $entity
+   *   The entity object to be checked.
+   *
+   * @return bool
+   *   TRUE if the entity has at least one translation in any revision, FALSE
+   *   otherwise.
+   *
+   * @see \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages()
+   * @see \Drupal\Core\Entity\ContentEntityStorageBase::isAnyRevisionTranslated()
+   */
+  protected function isAnyStoredRevisionTranslated(TranslatableInterface $entity) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    if ($entity->isNew()) {
+      return FALSE;
+    }
+
+    if ($entity instanceof TranslationStatusInterface) {
+      foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) {
+        if ($entity->getTranslationStatus($langcode) === TranslationStatusInterface::TRANSLATION_EXISTING) {
+          return TRUE;
+        }
+      }
+    }
+
+    $query = $this->getQuery()
+      ->condition($this->entityType->getKey('id'), $entity->id())
+      ->condition($this->entityType->getKey('default_langcode'), 0)
+      ->accessCheck(FALSE)
+      ->range(0, 1);
+
+    if ($entity->getEntityType()->isRevisionable()) {
+      $query->allRevisions();
+    }
+
+    $result = $query->execute();
+    return !empty($result);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -132,6 +230,148 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
     return $translation;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function createRevision(RevisionableInterface $entity, $default = TRUE, $keep_untranslatable_fields = NULL) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $new_revision = clone $entity;
+
+    // For translatable entities, create a merged revision of the active
+    // translation and the other translations in the default revision. This
+    // permits the creation of pending revisions that can always be saved as the
+    // new default revision without reverting changes in other languages.
+    if (!$entity->isNew() && !$entity->isDefaultRevision() && $entity->isTranslatable() && $this->isAnyRevisionTranslated($entity)) {
+      $active_langcode = $entity->language()->getId();
+      $skipped_field_names = array_flip($this->getRevisionTranslationMergeSkippedFieldNames());
+
+      // By default we copy untranslatable field values from the default
+      // revision, unless they are configured to affect only the default
+      // translation. This way we can ensure we always have only one affected
+      // translation in pending revisions. This constraint is enforced by
+      // EntityUntranslatableFieldsConstraintValidator.
+      if (!isset($keep_untranslatable_fields)) {
+        $keep_untranslatable_fields = $entity->isDefaultTranslation() && $entity->isDefaultTranslationAffectedOnly();
+      }
+
+      /** @var \Drupal\Core\Entity\ContentEntityInterface $default_revision */
+      $default_revision = $this->load($entity->id());
+      $translation_languages = $default_revision->getTranslationLanguages();
+      foreach ($translation_languages as $langcode => $language) {
+        if ($langcode == $active_langcode) {
+          continue;
+        }
+
+        $default_revision_translation = $default_revision->getTranslation($langcode);
+        $new_revision_translation = $new_revision->hasTranslation($langcode) ?
+          $new_revision->getTranslation($langcode) : $new_revision->addTranslation($langcode);
+
+        /** @var \Drupal\Core\Field\FieldItemListInterface[] $sync_items */
+        $sync_items = array_diff_key(
+          $keep_untranslatable_fields ? $default_revision_translation->getTranslatableFields() : $default_revision_translation->getFields(),
+          $skipped_field_names
+        );
+        foreach ($sync_items as $field_name => $items) {
+          $new_revision_translation->set($field_name, $items->getValue());
+        }
+
+        // Make sure the "revision_translation_affected" flag is recalculated.
+        $new_revision_translation->setRevisionTranslationAffected(NULL);
+
+        // No need to copy untranslatable field values more than once.
+        $keep_untranslatable_fields = TRUE;
+      }
+
+      // Make sure we do not inadvertently recreate removed translations.
+      foreach (array_diff_key($new_revision->getTranslationLanguages(), $translation_languages) as $langcode => $language) {
+        // Allow a new revision to be created for the active language.
+        if ($langcode !== $active_langcode) {
+          $new_revision->removeTranslation($langcode);
+        }
+      }
+
+      // The "original" property is used in various places to detect changes in
+      // field values with respect to the stored ones. If the property is not
+      // defined, the stored version is loaded explicitly. Since the merged
+      // revision generated here is not stored anywhere, we need to populate the
+      // "original" property manually, so that changes can be properly detected.
+      $new_revision->original = clone $new_revision;
+    }
+
+    // Eventually mark the new revision as such.
+    $new_revision->setNewRevision();
+    $new_revision->isDefaultRevision($default);
+
+    // Actually make sure the current translation is marked as affected, even if
+    // there are no explicit changes, to be sure this revision can be related
+    // to the correct translation.
+    $new_revision->setRevisionTranslationAffected(TRUE);
+
+    return $new_revision;
+  }
+
+  /**
+   * Returns an array of field names to skip when merging revision translations.
+   *
+   * @return array
+   *   An array of field names.
+   */
+  protected function getRevisionTranslationMergeSkippedFieldNames() {
+    /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
+    $entity_type = $this->getEntityType();
+
+    // A list of known revision metadata fields which should be skipped from
+    // the comparision.
+    $field_names = [
+      $entity_type->getKey('revision'),
+      $entity_type->getKey('revision_translation_affected'),
+    ];
+    $field_names = array_merge($field_names, array_values($entity_type->getRevisionMetadataKeys()));
+
+    return $field_names;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLatestRevisionId($entity_id) {
+    if (!$this->entityType->isRevisionable()) {
+      return NULL;
+    }
+
+    $result = $this->getQuery()
+      ->latestRevision()
+      ->condition($this->entityType->getKey('id'), $entity_id)
+      ->accessCheck(FALSE)
+      ->execute();
+
+    return key($result);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLatestTranslationAffectedRevisionId($entity_id, $langcode) {
+    if (!$this->entityType->isRevisionable()) {
+      return NULL;
+    }
+
+    if (!$this->entityType->isTranslatable()) {
+      return $this->getLatestRevisionId($entity_id);
+    }
+
+    $result = $this->getQuery()
+      ->allRevisions()
+      ->condition($this->entityType->getKey('id'), $entity_id)
+      ->condition($this->entityType->getKey('revision_translation_affected'), 1, '=', $langcode)
+      ->range(0, 1)
+      ->sort($this->entityType->getKey('revision'), 'DESC')
+      ->accessCheck(FALSE)
+      ->execute();
+
+    return key($result);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -210,15 +450,37 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
    * {@inheritdoc}
    */
   public function loadRevision($revision_id) {
-    $revision = $this->doLoadRevisionFieldItems($revision_id);
+    $revisions = $this->loadMultipleRevisions([$revision_id]);
+
+    return isset($revisions[$revision_id]) ? $revisions[$revision_id] : NULL;
+  }
 
-    if ($revision) {
-      $entities = [$revision->id() => $revision];
+  /**
+   * {@inheritdoc}
+   */
+  public function loadMultipleRevisions(array $revision_ids) {
+    $revisions = $this->doLoadMultipleRevisionsFieldItems($revision_ids);
+
+    // The hooks are executed with an array of entities keyed by the entity ID.
+    // As we could load multiple revisions for the same entity ID at once we
+    // have to build groups of entities where the same entity ID is present only
+    // once.
+    $entity_groups = [];
+    $entity_group_mapping = [];
+    foreach ($revisions as $revision) {
+      $entity_id = $revision->id();
+      $entity_group_key = isset($entity_group_mapping[$entity_id]) ? $entity_group_mapping[$entity_id] + 1 : 0;
+      $entity_group_mapping[$entity_id] = $entity_group_key;
+      $entity_groups[$entity_group_key][$entity_id] = $revision;
+    }
+
+    // Invoke the entity hooks for each group.
+    foreach ($entity_groups as $entities) {
       $this->invokeStorageLoadHook($entities);
       $this->postLoad($entities);
     }
 
-    return $revision;
+    return $revisions;
   }
 
   /**
@@ -229,9 +491,33 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
    *
    * @return \Drupal\Core\Entity\EntityInterface|null
    *   The specified entity revision or NULL if not found.
+   *
+   * @deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0.
+   *   \Drupal\Core\Entity\ContentEntityStorageBase::doLoadMultipleRevisionsFieldItems()
+   *   should be implemented instead.
+   *
+   * @see https://www.drupal.org/node/2924915
    */
   abstract protected function doLoadRevisionFieldItems($revision_id);
 
+  /**
+   * Actually loads revision field item values from the storage.
+   *
+   * @param array $revision_ids
+   *   An array of revision identifiers.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface[]
+   *   The specified entity revisions or an empty array if none are found.
+   */
+  protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
+    $revisions = [];
+    foreach ($revision_ids as $revision_id) {
+      $revisions[] = $this->doLoadRevisionFieldItems($revision_id);
+    }
+
+    return $revisions;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -254,6 +540,15 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
     }
 
     $this->populateAffectedRevisionTranslations($entity);
+
+    // Populate the "revision_default" flag. We skip this when we are resaving
+    // the revision because this is only allowed for default revisions, and
+    // these cannot be made non-default.
+    if ($this->entityType->isRevisionable() && $entity->isNewRevision()) {
+      $revision_default_key = $this->entityType->getRevisionMetadataKey('revision_default');
+      $entity->set($revision_default_key, $entity->isDefaultRevision());
+    }
+
     $this->doSaveFieldItems($entity);
 
     return $return;
@@ -553,32 +848,41 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con
       $languages = $entity->getTranslationLanguages();
       foreach ($languages as $langcode => $language) {
         $translation = $entity->getTranslation($langcode);
-        // Avoid populating the value if it was already manually set.
-        $affected = $translation->isRevisionTranslationAffected();
-        if (!isset($affected) && $translation->hasTranslationChanges()) {
-          $translation->setRevisionTranslationAffected(TRUE);
+        $current_affected = $translation->isRevisionTranslationAffected();
+        if (!isset($current_affected) || ($entity->isNewRevision() && !$translation->isRevisionTranslationAffectedEnforced())) {
+          // When setting the revision translation affected flag we have to
+          // explicitly set it to not be enforced. By default it will be
+          // enforced automatically when being set, which allows us to determine
+          // if the flag has been already set outside the storage in which case
+          // we should not recompute it.
+          // @see \Drupal\Core\Entity\ContentEntityBase::setRevisionTranslationAffected().
+          $new_affected = $translation->hasTranslationChanges() ? TRUE : NULL;
+          $translation->setRevisionTranslationAffected($new_affected);
+          $translation->setRevisionTranslationAffectedEnforced(FALSE);
         }
       }
     }
   }
 
   /**
-   * Ensures integer entity IDs are valid.
+   * Ensures integer entity key values are valid.
    *
    * The identifier sanitization provided by this method has been introduced
    * as Drupal used to rely on the database to facilitate this, which worked
    * correctly with MySQL but led to errors with other DBMS such as PostgreSQL.
    *
    * @param array $ids
-   *   The entity IDs to verify.
+   *   The entity key values to verify.
+   * @param string $entity_key
+   *   (optional) The entity key to sanitise values for. Defaults to 'id'.
    *
    * @return array
-   *   The sanitized list of entity IDs.
+   *   The sanitized list of entity key values.
    */
-  protected function cleanIds(array $ids) {
+  protected function cleanIds(array $ids, $entity_key = 'id') {
     $definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
-    $id_definition = $definitions[$this->entityType->getKey('id')];
-    if ($id_definition->getType() == 'integer') {
+    $field_name = $this->entityType->getKey($entity_key);
+    if ($field_name && $definitions[$field_name]->getType() == 'integer') {
       $ids = array_filter($ids, function ($id) {
         return is_numeric($id) && $id == (int) $id;
       });