c03cf4922036a6bf5afdb619aaf66924b41e7c2b
[yaffs-website] / web / core / modules / content_moderation / src / EntityTypeInfo.php
1 <?php
2
3 namespace Drupal\content_moderation;
4
5 use Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList;
6 use Drupal\Core\Entity\BundleEntityFormBase;
7 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 use Drupal\Core\Entity\EntityTypeInterface;
13 use Drupal\Core\Entity\EntityTypeManagerInterface;
14 use Drupal\Core\Field\BaseFieldDefinition;
15 use Drupal\Core\Form\FormInterface;
16 use Drupal\Core\Form\FormStateInterface;
17 use Drupal\Core\Session\AccountInterface;
18 use Drupal\Core\StringTranslation\StringTranslationTrait;
19 use Drupal\Core\StringTranslation\TranslationInterface;
20 use Drupal\content_moderation\Entity\Handler\BlockContentModerationHandler;
21 use Drupal\content_moderation\Entity\Handler\ModerationHandler;
22 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  * This class contains primarily bridged hooks for compile-time or
30  * cache-clear-time hooks. Runtime hooks should be placed in EntityOperations.
31  *
32  * @internal
33  */
34 class EntityTypeInfo implements ContainerInjectionInterface {
35
36   use StringTranslationTrait;
37
38   /**
39    * The moderation information service.
40    *
41    * @var \Drupal\content_moderation\ModerationInformationInterface
42    */
43   protected $moderationInfo;
44
45   /**
46    * The entity type manager.
47    *
48    * @var \Drupal\Core\Entity\EntityTypeManagerInterface
49    */
50   protected $entityTypeManager;
51
52   /**
53    * The bundle information service.
54    *
55    * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
56    */
57   protected $bundleInfo;
58
59   /**
60    * The current user.
61    *
62    * @var \Drupal\Core\Session\AccountInterface
63    */
64   protected $currentUser;
65
66   /**
67    * The state transition validation service.
68    *
69    * @var \Drupal\content_moderation\StateTransitionValidationInterface
70    */
71   protected $validator;
72
73   /**
74    * A keyed array of custom moderation handlers for given entity types.
75    *
76    * Any entity not specified will use a common default.
77    *
78    * @var array
79    */
80   protected $moderationHandlers = [
81     'node' => NodeModerationHandler::class,
82     'block_content' => BlockContentModerationHandler::class,
83   ];
84
85   /**
86    * EntityTypeInfo constructor.
87    *
88    * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
89    *   The translation service. for form alters.
90    * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
91    *   The moderation information service.
92    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
93    *   Entity type manager.
94    * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
95    *   Bundle information service.
96    * @param \Drupal\Core\Session\AccountInterface $current_user
97    *   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     $this->stringTranslation = $translation;
101     $this->moderationInfo = $moderation_information;
102     $this->entityTypeManager = $entity_type_manager;
103     $this->bundleInfo = $bundle_info;
104     $this->currentUser = $current_user;
105     $this->validator = $validator;
106   }
107
108   /**
109    * {@inheritdoc}
110    */
111   public static function create(ContainerInterface $container) {
112     return new static(
113       $container->get('string_translation'),
114       $container->get('content_moderation.moderation_information'),
115       $container->get('entity_type.manager'),
116       $container->get('entity_type.bundle.info'),
117       $container->get('current_user'),
118       $container->get('content_moderation.state_transition_validation')
119     );
120   }
121
122
123   /**
124    * Adds Moderation configuration to appropriate entity types.
125    *
126    * @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_types
127    *   The master entity type list to alter.
128    *
129    * @see hook_entity_type_alter()
130    */
131   public function entityTypeAlter(array &$entity_types) {
132     foreach ($entity_types as $entity_type_id => $entity_type) {
133       // The ContentModerationState entity type should never be moderated.
134       if ($entity_type->isRevisionable() && $entity_type_id != 'content_moderation_state') {
135         $entity_types[$entity_type_id] = $this->addModerationToEntityType($entity_type);
136       }
137     }
138   }
139
140   /**
141    * Modifies an entity definition to include moderation support.
142    *
143    * This primarily just means an extra handler. A Generic one is provided,
144    * but individual entity types can provide their own as appropriate.
145    *
146    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $type
147    *   The content entity definition to modify.
148    *
149    * @return \Drupal\Core\Entity\ContentEntityTypeInterface
150    *   The modified content entity definition.
151    */
152   protected function addModerationToEntityType(ContentEntityTypeInterface $type) {
153     if (!$type->hasHandlerClass('moderation')) {
154       $handler_class = !empty($this->moderationHandlers[$type->id()]) ? $this->moderationHandlers[$type->id()] : ModerationHandler::class;
155       $type->setHandlerClass('moderation', $handler_class);
156     }
157
158     if (!$type->hasLinkTemplate('latest-version') && $type->hasLinkTemplate('canonical')) {
159       $type->setLinkTemplate('latest-version', $type->getLinkTemplate('canonical') . '/latest');
160     }
161
162     $providers = $type->getRouteProviderClasses() ?: [];
163     if (empty($providers['moderation'])) {
164       $providers['moderation'] = EntityModerationRouteProvider::class;
165       $type->setHandlerClass('route_provider', $providers);
166     }
167
168     return $type;
169   }
170
171   /**
172    * Gets the "extra fields" for a bundle.
173    *
174    * @return array
175    *   A nested array of 'pseudo-field' elements. Each list is nested within the
176    *   following keys: entity type, bundle name, context (either 'form' or
177    *   'display'). The keys are the name of the elements as appearing in the
178    *   renderable array (either the entity form or the displayed entity). The
179    *   value is an associative array:
180    *   - label: The human readable name of the element. Make sure you sanitize
181    *     this appropriately.
182    *   - description: A short description of the element contents.
183    *   - weight: The default weight of the element.
184    *   - visible: (optional) The default visibility of the element. Defaults to
185    *     TRUE.
186    *   - edit: (optional) String containing markup (normally a link) used as the
187    *     element's 'edit' operation in the administration interface. Only for
188    *     'form' context.
189    *   - delete: (optional) String containing markup (normally a link) used as
190    *     the element's 'delete' operation in the administration interface. Only
191    *     for 'form' context.
192    *
193    * @see hook_entity_extra_field_info()
194    */
195   public function entityExtraFieldInfo() {
196     $return = [];
197     foreach ($this->getModeratedBundles() as $bundle) {
198       $return[$bundle['entity']][$bundle['bundle']]['display']['content_moderation_control'] = [
199         'label' => $this->t('Moderation control'),
200         'description' => $this->t("Status listing and form for the entity's moderation state."),
201         'weight' => -20,
202         'visible' => TRUE,
203       ];
204     }
205
206     return $return;
207   }
208
209   /**
210    * Returns an iterable list of entity names and bundle names under moderation.
211    *
212    * That is, this method returns a list of bundles that have Content
213    * Moderation enabled on them.
214    *
215    * @return \Generator
216    *   A generator, yielding a 2 element associative array:
217    *   - entity: The machine name of an entity type, such as "node" or
218    *     "block_content".
219    *   - bundle: The machine name of a bundle, such as "page" or "article".
220    */
221   protected function getModeratedBundles() {
222     $entity_types = array_filter($this->entityTypeManager->getDefinitions(), [$this->moderationInfo, 'canModerateEntitiesOfEntityType']);
223     foreach ($entity_types as $type_name => $type) {
224       foreach ($this->bundleInfo->getBundleInfo($type_name) as $bundle_id => $bundle) {
225         if ($this->moderationInfo->shouldModerateEntitiesOfBundle($type, $bundle_id)) {
226           yield ['entity' => $type_name, 'bundle' => $bundle_id];
227         }
228       }
229     }
230   }
231
232   /**
233    * Adds base field info to an entity type.
234    *
235    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
236    *   Entity type for adding base fields to.
237    *
238    * @return \Drupal\Core\Field\BaseFieldDefinition[]
239    *   New fields added by moderation state.
240    *
241    * @see hook_entity_base_field_info()
242    */
243   public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
244     if (!$this->moderationInfo->canModerateEntitiesOfEntityType($entity_type)) {
245       return [];
246     }
247
248     $fields = [];
249     $fields['moderation_state'] = BaseFieldDefinition::create('string')
250       ->setLabel(t('Moderation state'))
251       ->setDescription(t('The moderation state of this piece of content.'))
252       ->setComputed(TRUE)
253       ->setClass(ModerationStateFieldItemList::class)
254       ->setDisplayOptions('view', [
255         'label' => 'hidden',
256         'region' => 'hidden',
257         'weight' => -5,
258       ])
259       ->setDisplayOptions('form', [
260         'type' => 'moderation_state_default',
261         'weight' => 100,
262         'settings' => [],
263       ])
264       ->addConstraint('ModerationState', [])
265       ->setDisplayConfigurable('form', TRUE)
266       ->setDisplayConfigurable('view', FALSE)
267       ->setReadOnly(FALSE)
268       ->setTranslatable(TRUE);
269
270     return $fields;
271   }
272
273   /**
274    * Replaces the entity form entity object with a proper revision object.
275    *
276    * @param \Drupal\Core\Entity\EntityInterface $entity
277    *   The entity being edited.
278    * @param string $operation
279    *   The entity form operation.
280    * @param \Drupal\Core\Form\FormStateInterface $form_state
281    *   The form state.
282    *
283    * @see hook_entity_prepare_form()
284    */
285   public function entityPrepareForm(EntityInterface $entity, $operation, FormStateInterface $form_state) {
286     /** @var \Drupal\Core\Entity\EntityFormInterface $form_object */
287     $form_object = $form_state->getFormObject();
288
289     if ($this->isModeratedEntityEditForm($form_object) && !$entity->isNew()) {
290       // Generate a proper revision object for the current entity. This allows
291       // to correctly handle translatable entities having pending revisions.
292       /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
293       $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
294       /** @var \Drupal\Core\Entity\ContentEntityInterface $new_revision */
295       $new_revision = $storage->createRevision($entity, FALSE);
296
297       // Restore the revision ID as other modules may expect to find it still
298       // populated. This will reset the "new revision" flag, however the entity
299       // object will be marked as a new revision again on submit.
300       // @see \Drupal\Core\Entity\ContentEntityForm::buildEntity()
301       $revision_key = $new_revision->getEntityType()->getKey('revision');
302       $new_revision->set($revision_key, $new_revision->getLoadedRevisionId());
303       $form_object->setEntity($new_revision);
304     }
305   }
306
307   /**
308    * Alters bundle forms to enforce revision handling.
309    *
310    * @param array $form
311    *   An associative array containing the structure of the form.
312    * @param \Drupal\Core\Form\FormStateInterface $form_state
313    *   The current state of the form.
314    * @param string $form_id
315    *   The form id.
316    *
317    * @see hook_form_alter()
318    */
319   public function formAlter(array &$form, FormStateInterface $form_state, $form_id) {
320     $form_object = $form_state->getFormObject();
321     if ($form_object instanceof BundleEntityFormBase) {
322       $config_entity_type = $form_object->getEntity()->getEntityType();
323       $bundle_of = $config_entity_type->getBundleOf();
324       if ($bundle_of
325           && ($bundle_of_entity_type = $this->entityTypeManager->getDefinition($bundle_of))
326           && $this->moderationInfo->canModerateEntitiesOfEntityType($bundle_of_entity_type)) {
327         $this->entityTypeManager->getHandler($config_entity_type->getBundleOf(), 'moderation')->enforceRevisionsBundleFormAlter($form, $form_state, $form_id);
328       }
329     }
330     elseif ($this->isModeratedEntityEditForm($form_object)) {
331       /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
332       /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
333       $entity = $form_object->getEntity();
334       if ($this->moderationInfo->isModeratedEntity($entity)) {
335         $this->entityTypeManager
336           ->getHandler($entity->getEntityTypeId(), 'moderation')
337           ->enforceRevisionsEntityFormAlter($form, $form_state, $form_id);
338
339         // Submit handler to redirect to the latest version, if available.
340         $form['actions']['submit']['#submit'][] = [EntityTypeInfo::class, 'bundleFormRedirect'];
341
342         // Move the 'moderation_state' field widget to the footer region, if
343         // available.
344         if (isset($form['footer'])) {
345           $form['moderation_state']['#group'] = 'footer';
346         }
347
348         // If the publishing status exists in the meta region, replace it with
349         // the current state instead.
350         if (isset($form['meta']['published'])) {
351           $form['meta']['published']['#markup'] = $this->moderationInfo->getWorkflowForEntity($entity)->getTypePlugin()->getState($entity->moderation_state->value)->label();
352         }
353       }
354     }
355   }
356
357   /**
358    * Checks whether the specified form allows to edit a moderated entity.
359    *
360    * @param \Drupal\Core\Form\FormInterface $form_object
361    *   The form object.
362    *
363    * @return bool
364    *   TRUE if the form should get form moderation, FALSE otherwise.
365    */
366   protected function isModeratedEntityEditForm(FormInterface $form_object) {
367     return $form_object instanceof ContentEntityFormInterface &&
368       in_array($form_object->getOperation(), ['edit', 'default'], TRUE) &&
369       $this->moderationInfo->isModeratedEntity($form_object->getEntity());
370   }
371
372   /**
373    * Redirect content entity edit forms on save, if there is a pending revision.
374    *
375    * When saving their changes, editors should see those changes displayed on
376    * the next page.
377    *
378    * @param array $form
379    *   An associative array containing the structure of the form.
380    * @param \Drupal\Core\Form\FormStateInterface $form_state
381    *   The current state of the form.
382    */
383   public static function bundleFormRedirect(array &$form, FormStateInterface $form_state) {
384     /* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
385     $entity = $form_state->getFormObject()->getEntity();
386
387     $moderation_info = \Drupal::getContainer()->get('content_moderation.moderation_information');
388     if ($moderation_info->hasPendingRevision($entity) && $entity->hasLinkTemplate('latest-version')) {
389       $entity_type_id = $entity->getEntityTypeId();
390       $form_state->setRedirect("entity.$entity_type_id.latest_version", [$entity_type_id => $entity->id()]);
391     }
392   }
393
394 }