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