EntityTypeInfo.php 15.1 KB
Newer Older
1 2 3 4 5
<?php

namespace Drupal\content_moderation;

use Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList;
6 7
use Drupal\Core\Entity\BundleEntityFormBase;
use Drupal\Core\Entity\ContentEntityFormInterface;
8
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
9
use Drupal\Core\Entity\ContentEntityTypeInterface;
10
use Drupal\Core\Entity\EntityInterface;
11
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
12 13 14
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\BaseFieldDefinition;
15
use Drupal\Core\Form\FormInterface;
16
use Drupal\Core\Form\FormStateInterface;
17
use Drupal\Core\Session\AccountInterface;
18 19 20 21 22
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\content_moderation\Entity\Handler\BlockContentModerationHandler;
use Drupal\content_moderation\Entity\Handler\ModerationHandler;
use Drupal\content_moderation\Entity\Handler\NodeModerationHandler;
23
use Drupal\content_moderation\Entity\Routing\EntityModerationRouteProvider;
24
use Symfony\Component\DependencyInjection\ContainerInterface;
25 26

/**
27
 * Manipulates entity type information.
28 29 30
 *
 * This class contains primarily bridged hooks for compile-time or
 * cache-clear-time hooks. Runtime hooks should be placed in EntityOperations.
31 32
 *
 * @internal
33
 */
34
class EntityTypeInfo implements ContainerInjectionInterface {
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51

  use StringTranslationTrait;

  /**
   * The moderation information service.
   *
   * @var \Drupal\content_moderation\ModerationInformationInterface
   */
  protected $moderationInfo;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

52 53 54 55 56 57 58
  /**
   * The bundle information service.
   *
   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
   */
  protected $bundleInfo;

59 60 61 62 63 64 65
  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

66 67 68 69 70 71 72
  /**
   * The state transition validation service.
   *
   * @var \Drupal\content_moderation\StateTransitionValidationInterface
   */
  protected $validator;

73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
  /**
   * A keyed array of custom moderation handlers for given entity types.
   *
   * Any entity not specified will use a common default.
   *
   * @var array
   */
  protected $moderationHandlers = [
    'node' => NodeModerationHandler::class,
    'block_content' => BlockContentModerationHandler::class,
  ];

  /**
   * EntityTypeInfo constructor.
   *
   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
   *   The translation service. for form alters.
   * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
   *   The moderation information service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   Entity type manager.
94 95 96 97
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
   *   Bundle information service.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   Current user.
98
   */
99
  public function __construct(TranslationInterface $translation, ModerationInformationInterface $moderation_information, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info, AccountInterface $current_user, StateTransitionValidationInterface $validator) {
100 101 102
    $this->stringTranslation = $translation;
    $this->moderationInfo = $moderation_information;
    $this->entityTypeManager = $entity_type_manager;
103
    $this->bundleInfo = $bundle_info;
104
    $this->currentUser = $current_user;
105
    $this->validator = $validator;
106 107
  }

108 109 110 111 112 113 114 115
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('string_translation'),
      $container->get('content_moderation.moderation_information'),
      $container->get('entity_type.manager'),
116
      $container->get('entity_type.bundle.info'),
117 118
      $container->get('current_user'),
      $container->get('content_moderation.state_transition_validation')
119 120 121
    );
  }

122 123 124
  /**
   * Adds Moderation configuration to appropriate entity types.
   *
125
   * @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_types
126 127 128 129 130
   *   The master entity type list to alter.
   *
   * @see hook_entity_type_alter()
   */
  public function entityTypeAlter(array &$entity_types) {
131
    foreach ($entity_types as $entity_type_id => $entity_type) {
132 133 134 135 136
      // Internal entity types should never be moderated, and the 'path_alias'
      // entity type needs to be excluded for now.
      // @todo Enable moderation for path aliases after they become publishable
      //   in https://www.drupal.org/project/drupal/issues/3007669.
      if ($entity_type->isRevisionable() && !$entity_type->isInternal() && $entity_type_id !== 'path_alias') {
137 138
        $entity_types[$entity_type_id] = $this->addModerationToEntityType($entity_type);
      }
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
    }
  }

  /**
   * Modifies an entity definition to include moderation support.
   *
   * This primarily just means an extra handler. A Generic one is provided,
   * but individual entity types can provide their own as appropriate.
   *
   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $type
   *   The content entity definition to modify.
   *
   * @return \Drupal\Core\Entity\ContentEntityTypeInterface
   *   The modified content entity definition.
   */
154
  protected function addModerationToEntityType(ContentEntityTypeInterface $type) {
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
    if (!$type->hasHandlerClass('moderation')) {
      $handler_class = !empty($this->moderationHandlers[$type->id()]) ? $this->moderationHandlers[$type->id()] : ModerationHandler::class;
      $type->setHandlerClass('moderation', $handler_class);
    }

    if (!$type->hasLinkTemplate('latest-version') && $type->hasLinkTemplate('canonical')) {
      $type->setLinkTemplate('latest-version', $type->getLinkTemplate('canonical') . '/latest');
    }

    $providers = $type->getRouteProviderClasses() ?: [];
    if (empty($providers['moderation'])) {
      $providers['moderation'] = EntityModerationRouteProvider::class;
      $type->setHandlerClass('route_provider', $providers);
    }

    return $type;
  }

  /**
   * Gets the "extra fields" for a bundle.
   *
   * @return array
   *   A nested array of 'pseudo-field' elements. Each list is nested within the
   *   following keys: entity type, bundle name, context (either 'form' or
   *   'display'). The keys are the name of the elements as appearing in the
   *   renderable array (either the entity form or the displayed entity). The
   *   value is an associative array:
   *   - label: The human readable name of the element. Make sure you sanitize
   *     this appropriately.
   *   - description: A short description of the element contents.
   *   - weight: The default weight of the element.
   *   - visible: (optional) The default visibility of the element. Defaults to
   *     TRUE.
   *   - edit: (optional) String containing markup (normally a link) used as the
   *     element's 'edit' operation in the administration interface. Only for
   *     'form' context.
   *   - delete: (optional) String containing markup (normally a link) used as
   *     the element's 'delete' operation in the administration interface. Only
   *     for 'form' context.
194 195
   *
   * @see hook_entity_extra_field_info()
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
   */
  public function entityExtraFieldInfo() {
    $return = [];
    foreach ($this->getModeratedBundles() as $bundle) {
      $return[$bundle['entity']][$bundle['bundle']]['display']['content_moderation_control'] = [
        'label' => $this->t('Moderation control'),
        'description' => $this->t("Status listing and form for the entity's moderation state."),
        'weight' => -20,
        'visible' => TRUE,
      ];
    }

    return $return;
  }

  /**
   * Returns an iterable list of entity names and bundle names under moderation.
   *
   * That is, this method returns a list of bundles that have Content
   * Moderation enabled on them.
   *
   * @return \Generator
   *   A generator, yielding a 2 element associative array:
   *   - entity: The machine name of an entity type, such as "node" or
   *     "block_content".
   *   - bundle: The machine name of a bundle, such as "page" or "article".
   */
  protected function getModeratedBundles() {
224 225 226 227 228 229
    $entity_types = array_filter($this->entityTypeManager->getDefinitions(), [$this->moderationInfo, 'canModerateEntitiesOfEntityType']);
    foreach ($entity_types as $type_name => $type) {
      foreach ($this->bundleInfo->getBundleInfo($type_name) as $bundle_id => $bundle) {
        if ($this->moderationInfo->shouldModerateEntitiesOfBundle($type, $bundle_id)) {
          yield ['entity' => $type_name, 'bundle' => $bundle_id];
        }
230 231 232 233 234 235 236 237 238 239 240 241
      }
    }
  }

  /**
   * Adds base field info to an entity type.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   Entity type for adding base fields to.
   *
   * @return \Drupal\Core\Field\BaseFieldDefinition[]
   *   New fields added by moderation state.
242 243
   *
   * @see hook_entity_base_field_info()
244 245
   */
  public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
246
    if (!$this->moderationInfo->isModeratedEntityType($entity_type)) {
247 248 249 250
      return [];
    }

    $fields = [];
251 252 253
    $fields['moderation_state'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Moderation state'))
      ->setDescription(t('The moderation state of this piece of content.'))
254 255 256 257
      ->setComputed(TRUE)
      ->setClass(ModerationStateFieldItemList::class)
      ->setDisplayOptions('view', [
        'label' => 'hidden',
258
        'region' => 'hidden',
259 260 261 262
        'weight' => -5,
      ])
      ->setDisplayOptions('form', [
        'type' => 'moderation_state_default',
263
        'weight' => 100,
264 265 266
        'settings' => [],
      ])
      ->addConstraint('ModerationState', [])
267
      ->setDisplayConfigurable('form', TRUE)
268
      ->setDisplayConfigurable('view', FALSE)
269
      ->setReadOnly(FALSE)
270 271 272 273 274
      ->setTranslatable(TRUE);

    return $fields;
  }

275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
  /**
   * Replaces the entity form entity object with a proper revision object.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity being edited.
   * @param string $operation
   *   The entity form operation.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @see hook_entity_prepare_form()
   */
  public function entityPrepareForm(EntityInterface $entity, $operation, FormStateInterface $form_state) {
    /** @var \Drupal\Core\Entity\EntityFormInterface $form_object */
    $form_object = $form_state->getFormObject();

    if ($this->isModeratedEntityEditForm($form_object) && !$entity->isNew()) {
      // Generate a proper revision object for the current entity. This allows
      // to correctly handle translatable entities having pending revisions.
      /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
      $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
      /** @var \Drupal\Core\Entity\ContentEntityInterface $new_revision */
      $new_revision = $storage->createRevision($entity, FALSE);

      // Restore the revision ID as other modules may expect to find it still
      // populated. This will reset the "new revision" flag, however the entity
      // object will be marked as a new revision again on submit.
      // @see \Drupal\Core\Entity\ContentEntityForm::buildEntity()
      $revision_key = $new_revision->getEntityType()->getKey('revision');
      $new_revision->set($revision_key, $new_revision->getLoadedRevisionId());
      $form_object->setEntity($new_revision);
    }
  }

309 310 311 312 313 314 315 316 317 318 319 320
  /**
   * Alters bundle forms to enforce revision handling.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   * @param string $form_id
   *   The form id.
   *
   * @see hook_form_alter()
   */
321 322 323
  public function formAlter(array &$form, FormStateInterface $form_state, $form_id) {
    $form_object = $form_state->getFormObject();
    if ($form_object instanceof BundleEntityFormBase) {
324 325
      $config_entity = $form_object->getEntity();
      $bundle_of = $config_entity->getEntityType()->getBundleOf();
326 327
      if ($bundle_of
          && ($bundle_of_entity_type = $this->entityTypeManager->getDefinition($bundle_of))
328 329
          && $this->moderationInfo->shouldModerateEntitiesOfBundle($bundle_of_entity_type, $config_entity->id())) {
        $this->entityTypeManager->getHandler($bundle_of, 'moderation')->enforceRevisionsBundleFormAlter($form, $form_state, $form_id);
330
      }
331
    }
332 333 334
    elseif ($this->isModeratedEntityEditForm($form_object)) {
      /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
      /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
335
      $entity = $form_object->getEntity();
336

337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
      $this->entityTypeManager
        ->getHandler($entity->getEntityTypeId(), 'moderation')
        ->enforceRevisionsEntityFormAlter($form, $form_state, $form_id);

      // Submit handler to redirect to the latest version, if available.
      $form['actions']['submit']['#submit'][] = [EntityTypeInfo::class, 'bundleFormRedirect'];

      // Move the 'moderation_state' field widget to the footer region, if
      // available.
      if (isset($form['footer']) && in_array($form_object->getOperation(), ['edit', 'default'], TRUE)) {
        $form['moderation_state']['#group'] = 'footer';
      }

      // If the publishing status exists in the meta region, replace it with
      // the current state instead.
      if (isset($form['meta']['published'])) {
        $form['meta']['published']['#markup'] = $this->moderationInfo->getWorkflowForEntity($entity)->getTypePlugin()->getState($entity->moderation_state->value)->label();
354
      }
355 356 357
    }
  }

358 359 360 361 362 363 364 365 366 367 368
  /**
   * Checks whether the specified form allows to edit a moderated entity.
   *
   * @param \Drupal\Core\Form\FormInterface $form_object
   *   The form object.
   *
   * @return bool
   *   TRUE if the form should get form moderation, FALSE otherwise.
   */
  protected function isModeratedEntityEditForm(FormInterface $form_object) {
    return $form_object instanceof ContentEntityFormInterface &&
369
      in_array($form_object->getOperation(), ['edit', 'default', 'layout_builder'], TRUE) &&
370 371 372
      $this->moderationInfo->isModeratedEntity($form_object->getEntity());
  }

373
  /**
374
   * Redirect content entity edit forms on save, if there is a pending revision.
375 376 377 378 379 380 381 382 383 384 385 386 387 388
   *
   * When saving their changes, editors should see those changes displayed on
   * the next page.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  public static function bundleFormRedirect(array &$form, FormStateInterface $form_state) {
    /* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    $entity = $form_state->getFormObject()->getEntity();

    $moderation_info = \Drupal::getContainer()->get('content_moderation.moderation_information');
389
    if ($moderation_info->hasPendingRevision($entity) && $entity->hasLinkTemplate('latest-version')) {
390 391 392 393 394 395
      $entity_type_id = $entity->getEntityTypeId();
      $form_state->setRedirect("entity.$entity_type_id.latest_version", [$entity_type_id => $entity->id()]);
    }
  }

}