Pull merge.
[yaffs-website] / web / core / modules / content_moderation / src / EntityOperations.php
index 3f66100581bdc5280ff8329a069656d62a88b005..8c6d3325089840426b950cfebeaef7f20fe03ad8 100644 (file)
@@ -3,19 +3,24 @@
 namespace Drupal\content_moderation;
 
 use Drupal\content_moderation\Entity\ContentModerationState as ContentModerationStateEntity;
+use Drupal\content_moderation\Entity\ContentModerationStateInterface;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityPublishedInterface;
 use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormBuilderInterface;
-use Drupal\Core\TypedData\TranslatableInterface;
 use Drupal\content_moderation\Form\EntityModerationForm;
-use Drupal\workflows\WorkflowInterface;
+use Drupal\Core\Routing\RouteBuilderInterface;
+use Drupal\workflows\Entity\Workflow;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Defines a class for reacting to entity events.
+ *
+ * @internal
  */
 class EntityOperations implements ContainerInjectionInterface {
 
@@ -41,18 +46,18 @@ class EntityOperations implements ContainerInjectionInterface {
   protected $formBuilder;
 
   /**
-   * The Revision Tracker service.
+   * The entity bundle information service.
    *
-   * @var \Drupal\content_moderation\RevisionTrackerInterface
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
    */
-  protected $tracker;
+  protected $bundleInfo;
 
   /**
-   * The entity bundle information service.
+   * The router builder service.
    *
-   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+   * @var \Drupal\Core\Routing\RouteBuilderInterface
    */
-  protected $bundleInfo;
+  protected $routerBuilder;
 
   /**
    * Constructs a new EntityOperations object.
@@ -63,17 +68,17 @@ class EntityOperations implements ContainerInjectionInterface {
    *   Entity type manager service.
    * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
    *   The form builder.
-   * @param \Drupal\content_moderation\RevisionTrackerInterface $tracker
-   *   The revision tracker.
    * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
    *   The entity bundle information service.
+   * @param \Drupal\Core\Routing\RouteBuilderInterface $router_builder
+   *   The router builder service.
    */
-  public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, RevisionTrackerInterface $tracker, EntityTypeBundleInfoInterface $bundle_info) {
+  public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, EntityTypeBundleInfoInterface $bundle_info, RouteBuilderInterface $router_builder) {
     $this->moderationInfo = $moderation_info;
     $this->entityTypeManager = $entity_type_manager;
     $this->formBuilder = $form_builder;
-    $this->tracker = $tracker;
     $this->bundleInfo = $bundle_info;
+    $this->routerBuilder = $router_builder;
   }
 
   /**
@@ -84,8 +89,8 @@ class EntityOperations implements ContainerInjectionInterface {
       $container->get('content_moderation.moderation_information'),
       $container->get('entity_type.manager'),
       $container->get('form_builder'),
-      $container->get('content_moderation.revision_tracker'),
-      $container->get('entity_type.bundle.info')
+      $container->get('entity_type.bundle.info'),
+      $container->get('router.builder')
     );
   }
 
@@ -94,6 +99,8 @@ class EntityOperations implements ContainerInjectionInterface {
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   The entity being saved.
+   *
+   * @see hook_entity_presave()
    */
   public function entityPresave(EntityInterface $entity) {
     if (!$this->moderationInfo->isModeratedEntity($entity)) {
@@ -103,23 +110,23 @@ class EntityOperations implements ContainerInjectionInterface {
     if ($entity->moderation_state->value) {
       $workflow = $this->moderationInfo->getWorkflowForEntity($entity);
       /** @var \Drupal\content_moderation\ContentModerationState $current_state */
-      $current_state = $workflow->getState($entity->moderation_state->value);
+      $current_state = $workflow->getTypePlugin()
+        ->getState($entity->moderation_state->value);
 
-      // This entity is default if it is new, a new translation, the default
-      // revision, or the default revision is not published.
+      // This entity is default if it is new, the default revision, or the
+      // default revision is not published.
       $update_default_revision = $entity->isNew()
-        || $entity->isNewTranslation()
         || $current_state->isDefaultRevisionState()
-        || !$this->isDefaultRevisionPublished($entity, $workflow);
+        || !$this->moderationInfo->isDefaultRevisionPublished($entity);
 
       // Fire per-entity-type logic for handling the save process.
-      $this->entityTypeManager->getHandler($entity->getEntityTypeId(), 'moderation')->onPresave($entity, $update_default_revision, $current_state->isPublishedState());
+      $this->entityTypeManager
+        ->getHandler($entity->getEntityTypeId(), 'moderation')
+        ->onPresave($entity, $update_default_revision, $current_state->isPublishedState());
     }
   }
 
   /**
-   * Hook bridge.
-   *
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   The entity that was just saved.
    *
@@ -128,13 +135,10 @@ class EntityOperations implements ContainerInjectionInterface {
   public function entityInsert(EntityInterface $entity) {
     if ($this->moderationInfo->isModeratedEntity($entity)) {
       $this->updateOrCreateFromEntity($entity);
-      $this->setLatestRevision($entity);
     }
   }
 
   /**
-   * Hook bridge.
-   *
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   The entity that was just saved.
    *
@@ -143,7 +147,12 @@ class EntityOperations implements ContainerInjectionInterface {
   public function entityUpdate(EntityInterface $entity) {
     if ($this->moderationInfo->isModeratedEntity($entity)) {
       $this->updateOrCreateFromEntity($entity);
-      $this->setLatestRevision($entity);
+    }
+    // When updating workflow settings for Content Moderation, we need to
+    // rebuild routes as we may be enabling new entity types and the related
+    // entity forms.
+    elseif ($entity instanceof Workflow && $entity->getTypePlugin()->getPluginId() == 'content_moderation') {
+      $this->routerBuilder->setRebuildNeeded();
     }
   }
 
@@ -154,131 +163,161 @@ class EntityOperations implements ContainerInjectionInterface {
    *   The entity to update or create a moderation state for.
    */
   protected function updateOrCreateFromEntity(EntityInterface $entity) {
-    $moderation_state = $entity->moderation_state->value;
-    $workflow = $this->moderationInfo->getWorkflowForEntity($entity);
     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
-    if (!$moderation_state) {
-      $moderation_state = $workflow->getTypePlugin()->getInitialState($workflow, $entity)->id();
-    }
-
-    // @todo what if $entity->moderation_state is null at this point?
-    $entity_type_id = $entity->getEntityTypeId();
-    $entity_id = $entity->id();
     $entity_revision_id = $entity->getRevisionId();
-
+    $workflow = $this->moderationInfo->getWorkflowForEntity($entity);
+    $content_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($entity);
+    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
     $storage = $this->entityTypeManager->getStorage('content_moderation_state');
-    $entities = $storage->loadByProperties([
-      'content_entity_type_id' => $entity_type_id,
-      'content_entity_id' => $entity_id,
-      'workflow' => $workflow->id(),
-    ]);
 
-    /** @var \Drupal\content_moderation\ContentModerationStateInterface $content_moderation_state */
-    $content_moderation_state = reset($entities);
     if (!($content_moderation_state instanceof ContentModerationStateInterface)) {
       $content_moderation_state = $storage->create([
-        'content_entity_type_id' => $entity_type_id,
-        'content_entity_id' => $entity_id,
+        'content_entity_type_id' => $entity->getEntityTypeId(),
+        'content_entity_id' => $entity->id(),
+        // Make sure that the moderation state entity has the same language code
+        // as the moderated entity.
+        'langcode' => $entity->language()->getId(),
       ]);
       $content_moderation_state->workflow->target_id = $workflow->id();
     }
-    elseif ($content_moderation_state->content_entity_revision_id->value != $entity_revision_id) {
-      // If a new revision of the content has been created, add a new content
-      // moderation state revision.
-      $content_moderation_state->setNewRevision(TRUE);
-    }
 
     // Sync translations.
     if ($entity->getEntityType()->hasKey('langcode')) {
       $entity_langcode = $entity->language()->getId();
-      if (!$content_moderation_state->hasTranslation($entity_langcode)) {
-        $content_moderation_state->addTranslation($entity_langcode);
+      if ($entity->isDefaultTranslation()) {
+        $content_moderation_state->langcode = $entity_langcode;
       }
-      if ($content_moderation_state->language()->getId() !== $entity_langcode) {
-        $content_moderation_state = $content_moderation_state->getTranslation($entity_langcode);
+      else {
+        if (!$content_moderation_state->hasTranslation($entity_langcode)) {
+          $content_moderation_state->addTranslation($entity_langcode);
+        }
+        if ($content_moderation_state->language()->getId() !== $entity_langcode) {
+          $content_moderation_state = $content_moderation_state->getTranslation($entity_langcode);
+        }
       }
     }
 
+    // If a new revision of the content has been created, add a new content
+    // moderation state revision.
+    if (!$content_moderation_state->isNew() && $content_moderation_state->content_entity_revision_id->value != $entity_revision_id) {
+      $content_moderation_state = $storage->createRevision($content_moderation_state, $entity->isDefaultRevision());
+    }
+
     // Create the ContentModerationState entity for the inserted entity.
+    $moderation_state = $entity->moderation_state->value;
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    if (!$moderation_state) {
+      $moderation_state = $workflow->getTypePlugin()->getInitialState($entity)->id();
+    }
+
     $content_moderation_state->set('content_entity_revision_id', $entity_revision_id);
     $content_moderation_state->set('moderation_state', $moderation_state);
     ContentModerationStateEntity::updateOrCreateFromEntity($content_moderation_state);
   }
 
   /**
-   * Set the latest revision.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being deleted.
    *
+   * @see hook_entity_delete()
+   */
+  public function entityDelete(EntityInterface $entity) {
+    $content_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($entity);
+    if ($content_moderation_state) {
+      $content_moderation_state->delete();
+    }
+  }
+
+  /**
    * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   The content entity to create content_moderation_state entity for.
+   *   The entity revision being deleted.
+   *
+   * @see hook_entity_revision_delete()
    */
-  protected function setLatestRevision(EntityInterface $entity) {
+  public function entityRevisionDelete(EntityInterface $entity) {
     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
-    $this->tracker->setLatestRevision(
-      $entity->getEntityTypeId(),
-      $entity->id(),
-      $entity->language()->getId(),
-      $entity->getRevisionId()
-    );
+    if (!$entity->isDefaultRevision()) {
+      $content_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($entity);
+      if ($content_moderation_state) {
+        $this->entityTypeManager
+          ->getStorage('content_moderation_state')
+          ->deleteRevision($content_moderation_state->getRevisionId());
+      }
+    }
   }
 
   /**
-   * Act on entities being assembled before rendering.
+   * @param \Drupal\Core\Entity\EntityInterface $translation
+   *   The entity translation being deleted.
    *
-   * This is a hook bridge.
+   * @see hook_entity_translation_delete()
+   */
+  public function entityTranslationDelete(EntityInterface $translation) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $translation */
+    if (!$translation->isDefaultTranslation()) {
+      $langcode = $translation->language()->getId();
+      $content_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($translation);
+      if ($content_moderation_state && $content_moderation_state->hasTranslation($langcode)) {
+        $content_moderation_state->removeTranslation($langcode);
+        ContentModerationStateEntity::updateOrCreateFromEntity($content_moderation_state);
+      }
+    }
+  }
+
+  /**
+   * Act on entities being assembled before rendering.
    *
    * @see hook_entity_view()
    * @see EntityFieldManagerInterface::getExtraFields()
    */
   public function entityView(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
     if (!$this->moderationInfo->isModeratedEntity($entity)) {
       return;
     }
-    if (!$this->moderationInfo->isLatestRevision($entity)) {
+    if (isset($entity->in_preview) && $entity->in_preview) {
       return;
     }
-    if ($this->moderationInfo->isLiveRevision($entity)) {
+    // If the component is not defined for this display, we have nothing to do.
+    if (!$display->getComponent('content_moderation_control')) {
       return;
     }
-
-    $component = $display->getComponent('content_moderation_control');
-    if ($component) {
-      $build['content_moderation_control'] = $this->formBuilder->getForm(EntityModerationForm::class, $entity);
-      $build['content_moderation_control']['#weight'] = $component['weight'];
+    // The moderation form should be displayed only when viewing the latest
+    // (translation-affecting) revision, unless it was created as published
+    // default revision.
+    if (($entity->isDefaultRevision() || $entity->wasDefaultRevision()) && $this->isPublished($entity)) {
+      return;
     }
+    if (!$entity->isLatestRevision() && !$entity->isLatestTranslationAffectedRevision()) {
+      return;
+    }
+
+    $build['content_moderation_control'] = $this->formBuilder->getForm(EntityModerationForm::class, $entity);
   }
 
   /**
-   * Check if the default revision for the given entity is published.
+   * Checks if the entity is published.
    *
-   * The default revision is the same as the entity retrieved by "default" from
-   * the storage handler. If the entity is translated, check if any of the
-   * translations are published.
+   * This method is optimized to not have to unnecessarily load the moderation
+   * state and workflow if it is not required.
    *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   The entity being saved.
-   * @param \Drupal\workflows\WorkflowInterface $workflow
-   *   The workflow being applied to the entity.
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   The entity to check.
    *
    * @return bool
-   *   TRUE if the default revision is published. FALSE otherwise.
+   *   TRUE if the entity is published, FALSE otherwise.
    */
-  protected function isDefaultRevisionPublished(EntityInterface $entity, WorkflowInterface $workflow) {
-    $default_revision = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->load($entity->id());
-
-    // Ensure we are checking all translations of the default revision.
-    if ($default_revision instanceof TranslatableInterface && $default_revision->isTranslatable()) {
-      // Loop through each language that has a translation.
-      foreach ($default_revision->getTranslationLanguages() as $language) {
-        // Load the translated revision.
-        $language_revision = $default_revision->getTranslation($language->getId());
-        // Return TRUE if a translation with a published state is found.
-        if ($workflow->getState($language_revision->moderation_state->value)->isPublishedState()) {
-          return TRUE;
-        }
-      }
+  protected function isPublished(ContentEntityInterface $entity) {
+    // If the entity implements EntityPublishedInterface directly, check that
+    // first, otherwise fall back to check through the workflow state.
+    if ($entity instanceof EntityPublishedInterface) {
+      return $entity->isPublished();
     }
-
-    return $workflow->getState($default_revision->moderation_state->value)->isPublishedState();
+    if ($moderation_state = $entity->get('moderation_state')->value) {
+      $workflow = $this->moderationInfo->getWorkflowForEntity($entity);
+      return $workflow->getTypePlugin()->getState($moderation_state)->isPublishedState();
+    }
+    return FALSE;
   }
 
 }