4b07e45790b44deab1f302bc8a3eeeb27e6990bd
[yaffs-website] / web / core / modules / content_moderation / content_moderation.module
1 <?php
2
3 /**
4  * @file
5  * Contains content_moderation.module.
6  */
7
8 use Drupal\content_moderation\EntityOperations;
9 use Drupal\content_moderation\EntityTypeInfo;
10 use Drupal\content_moderation\ContentPreprocess;
11 use Drupal\content_moderation\Plugin\Action\ModerationOptOutPublish;
12 use Drupal\content_moderation\Plugin\Action\ModerationOptOutUnpublish;
13 use Drupal\Core\Access\AccessResult;
14 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
15 use Drupal\Core\Entity\EntityInterface;
16 use Drupal\Core\Entity\EntityPublishedInterface;
17 use Drupal\Core\Entity\EntityTypeInterface;
18 use Drupal\Core\Field\FieldDefinitionInterface;
19 use Drupal\Core\Field\FieldItemListInterface;
20 use Drupal\Core\Form\FormStateInterface;
21 use Drupal\Core\Routing\RouteMatchInterface;
22 use Drupal\Core\Session\AccountInterface;
23 use Drupal\Core\Url;
24 use Drupal\workflows\WorkflowInterface;
25 use Drupal\Core\Action\Plugin\Action\PublishAction;
26 use Drupal\Core\Action\Plugin\Action\UnpublishAction;
27 use Drupal\workflows\Entity\Workflow;
28 use Drupal\views\Entity\View;
29
30 /**
31  * Implements hook_help().
32  */
33 function content_moderation_help($route_name, RouteMatchInterface $route_match) {
34   switch ($route_name) {
35     // Main module help for the content_moderation module.
36     case 'help.page.content_moderation':
37       $output = '';
38       $output .= '<h3>' . t('About') . '</h3>';
39       $output .= '<p>' . t('The Content Moderation module allows you to expand on Drupal\'s "unpublished" and "published" states for content. It allows you to have a published version that is live, but have a separate working copy that is undergoing review before it is published. This is achieved by using <a href=":workflows">Workflows</a> to apply different states and transitions to entities as needed. For more information, see the <a href=":content_moderation">online documentation for the Content Moderation module</a>.', [':content_moderation' => 'https://www.drupal.org/documentation/modules/content_moderation', ':workflows' => Url::fromRoute('help.page', ['name' => 'workflows'])->toString()]) . '</p>';
40       $output .= '<h3>' . t('Uses') . '</h3>';
41       $output .= '<dl>';
42       $output .= '<dt>' . t('Applying workflows') . '</dt>';
43       $output .= '<dd>' . t('Content Moderation allows you to apply <a href=":workflows">Workflows</a> to content, custom blocks, and other <a href=":field_help" title="Field module help, with background on content entities">content entities</a>, to provide more fine-grained publishing options. For example, a Basic page might have states such as Draft and Published, with allowed transitions such as Draft to Published (making the current revision "live"), and Published to Draft (making a new draft revision of published content).', [':workflows' => Url::fromRoute('help.page', ['name' => 'workflows'])->toString(), ':field_help' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '</dd>';
44       if (\Drupal::moduleHandler()->moduleExists('views')) {
45         $moderated_content_view = View::load('moderated_content');
46         if (isset($moderated_content_view) && $moderated_content_view->status() === TRUE) {
47           $output .= '<dt>' . t('Moderating content') . '</dt>';
48           $output .= '<dd>' . t('You can view a list of content awaiting moderation on the <a href=":moderated">moderated content page</a>. This will show any content in an unpublished state, such as Draft or Archived, to help surface content that requires more work from content editors.', [':moderated' => Url::fromRoute('view.moderated_content.moderated_content')->toString()]) . '</dd>';
49         }
50       }
51       $output .= '<dt>' . t('Configure Content Moderation permissions') . '</dt>';
52       $output .= '<dd>' . t('Each transition is exposed as a permission. If a user has the permission for a transition, they can use the transition to change the state of the content item, from Draft to Published.') . '</dd>';
53       $output .= '</dl>';
54       return $output;
55   }
56 }
57
58 /**
59  * Implements hook_entity_base_field_info().
60  */
61 function content_moderation_entity_base_field_info(EntityTypeInterface $entity_type) {
62   return \Drupal::service('class_resolver')
63     ->getInstanceFromDefinition(EntityTypeInfo::class)
64     ->entityBaseFieldInfo($entity_type);
65 }
66
67 /**
68  * Implements hook_entity_type_alter().
69  */
70 function content_moderation_entity_type_alter(array &$entity_types) {
71   \Drupal::service('class_resolver')
72     ->getInstanceFromDefinition(EntityTypeInfo::class)
73     ->entityTypeAlter($entity_types);
74 }
75
76 /**
77  * Implements hook_entity_presave().
78  */
79 function content_moderation_entity_presave(EntityInterface $entity) {
80   return \Drupal::service('class_resolver')
81     ->getInstanceFromDefinition(EntityOperations::class)
82     ->entityPresave($entity);
83 }
84
85 /**
86  * Implements hook_entity_insert().
87  */
88 function content_moderation_entity_insert(EntityInterface $entity) {
89   return \Drupal::service('class_resolver')
90     ->getInstanceFromDefinition(EntityOperations::class)
91     ->entityInsert($entity);
92 }
93
94 /**
95  * Implements hook_entity_update().
96  */
97 function content_moderation_entity_update(EntityInterface $entity) {
98   return \Drupal::service('class_resolver')
99     ->getInstanceFromDefinition(EntityOperations::class)
100     ->entityUpdate($entity);
101 }
102
103 /**
104  * Implements hook_entity_delete().
105  */
106 function content_moderation_entity_delete(EntityInterface $entity) {
107   return \Drupal::service('class_resolver')
108     ->getInstanceFromDefinition(EntityOperations::class)
109     ->entityDelete($entity);
110 }
111
112 /**
113  * Implements hook_entity_revision_delete().
114  */
115 function content_moderation_entity_revision_delete(EntityInterface $entity) {
116   return \Drupal::service('class_resolver')
117     ->getInstanceFromDefinition(EntityOperations::class)
118     ->entityRevisionDelete($entity);
119 }
120
121 /**
122  * Implements hook_entity_translation_delete().
123  */
124 function content_moderation_entity_translation_delete(EntityInterface $translation) {
125   return \Drupal::service('class_resolver')
126     ->getInstanceFromDefinition(EntityOperations::class)
127     ->entityTranslationDelete($translation);
128 }
129
130 /**
131  * Implements hook_entity_prepare_form().
132  */
133 function content_moderation_entity_prepare_form(EntityInterface $entity, $operation, FormStateInterface $form_state) {
134   \Drupal::service('class_resolver')
135     ->getInstanceFromDefinition(EntityTypeInfo::class)
136     ->entityPrepareForm($entity, $operation, $form_state);
137 }
138
139 /**
140  * Implements hook_form_alter().
141  */
142 function content_moderation_form_alter(&$form, FormStateInterface $form_state, $form_id) {
143   \Drupal::service('class_resolver')
144     ->getInstanceFromDefinition(EntityTypeInfo::class)
145     ->formAlter($form, $form_state, $form_id);
146 }
147
148 /**
149  * Implements hook_preprocess_HOOK().
150  */
151 function content_moderation_preprocess_node(&$variables) {
152   \Drupal::service('class_resolver')
153     ->getInstanceFromDefinition(ContentPreprocess::class)
154     ->preprocessNode($variables);
155 }
156
157 /**
158  * Implements hook_entity_extra_field_info().
159  */
160 function content_moderation_entity_extra_field_info() {
161   return \Drupal::service('class_resolver')
162     ->getInstanceFromDefinition(EntityTypeInfo::class)
163     ->entityExtraFieldInfo();
164 }
165
166 /**
167  * Implements hook_entity_view().
168  */
169 function content_moderation_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
170   \Drupal::service('class_resolver')
171     ->getInstanceFromDefinition(EntityOperations::class)
172     ->entityView($build, $entity, $display, $view_mode);
173 }
174
175 /**
176  * Implements hook_entity_access().
177  *
178  * Entities should be viewable if unpublished and the user has the appropriate
179  * permission. This permission is therefore effectively mandatory for any user
180  * that wants to moderate things.
181  */
182 function content_moderation_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
183   /** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
184   $moderation_info = Drupal::service('content_moderation.moderation_information');
185
186   $access_result = NULL;
187   if ($operation === 'view') {
188     $access_result = (($entity instanceof EntityPublishedInterface) && !$entity->isPublished())
189       ? AccessResult::allowedIfHasPermission($account, 'view any unpublished content')
190       : AccessResult::neutral();
191
192     $access_result->addCacheableDependency($entity);
193   }
194   elseif ($operation === 'update' && $moderation_info->isModeratedEntity($entity) && $entity->moderation_state) {
195     /** @var \Drupal\content_moderation\StateTransitionValidation $transition_validation */
196     $transition_validation = \Drupal::service('content_moderation.state_transition_validation');
197
198     $valid_transition_targets = $transition_validation->getValidTransitions($entity, $account);
199     $access_result = $valid_transition_targets ? AccessResult::neutral() : AccessResult::forbidden();
200
201     $access_result->addCacheableDependency($entity);
202     $access_result->addCacheableDependency($account);
203     $workflow = $moderation_info->getWorkflowForEntity($entity);
204     $access_result->addCacheableDependency($workflow);
205     foreach ($valid_transition_targets as $valid_transition_target) {
206       $access_result->addCacheableDependency($valid_transition_target);
207     }
208   }
209
210   return $access_result;
211 }
212
213 /**
214  * Implements hook_entity_field_access().
215  */
216 function content_moderation_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
217   if ($items && $operation === 'edit') {
218     /** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
219     $moderation_info = Drupal::service('content_moderation.moderation_information');
220
221     $entity_type = \Drupal::entityTypeManager()->getDefinition($field_definition->getTargetEntityTypeId());
222
223     $entity = $items->getEntity();
224
225     // Deny edit access to the published field if the entity is being moderated.
226     if ($entity_type->hasKey('published') && $moderation_info->isModeratedEntity($entity) && $entity->moderation_state && $field_definition->getName() == $entity_type->getKey('published')) {
227       return AccessResult::forbidden();
228     }
229   }
230
231   return AccessResult::neutral();
232 }
233
234 /**
235  * Implements hook_theme().
236  */
237 function content_moderation_theme() {
238   return ['entity_moderation_form' => ['render element' => 'form']];
239 }
240
241 /**
242  * Implements hook_action_info_alter().
243  */
244 function content_moderation_action_info_alter(&$definitions) {
245
246   // The publish/unpublish actions are not valid on moderated entities. So swap
247   // their implementations out for alternates that will become a no-op on a
248   // moderated entity. If another module has already swapped out those classes,
249   // though, we'll be polite and do nothing.
250   foreach ($definitions as &$definition) {
251     if ($definition['id'] === 'entity:publish_action' && $definition['class'] == PublishAction::class) {
252       $definition['class'] = ModerationOptOutPublish::class;
253     }
254     if ($definition['id'] === 'entity:unpublish_action' && $definition['class'] == UnpublishAction::class) {
255       $definition['class'] = ModerationOptOutUnpublish::class;
256     }
257   }
258 }
259
260 /**
261  * Implements hook_entity_bundle_info_alter().
262  */
263 function content_moderation_entity_bundle_info_alter(&$bundles) {
264   $translatable = FALSE;
265   /** @var \Drupal\workflows\WorkflowInterface $workflow */
266   foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
267     /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */
268     $plugin = $workflow->getTypePlugin();
269     foreach ($plugin->getEntityTypes() as $entity_type_id) {
270       foreach ($plugin->getBundlesForEntityType($entity_type_id) as $bundle_id) {
271         if (isset($bundles[$entity_type_id][$bundle_id])) {
272           $bundles[$entity_type_id][$bundle_id]['workflow'] = $workflow->id();
273           // If we have even one moderation-enabled translatable bundle, we need
274           // to make the moderation state bundle translatable as well, to enable
275           // the revision translation merge logic also for content moderation
276           // state revisions.
277           if (!empty($bundles[$entity_type_id][$bundle_id]['translatable'])) {
278             $translatable = TRUE;
279           }
280         }
281       }
282     }
283   }
284   $bundles['content_moderation_state']['content_moderation_state']['translatable'] = $translatable;
285 }
286
287 /**
288  * Implements hook_entity_bundle_delete().
289  */
290 function content_moderation_entity_bundle_delete($entity_type_id, $bundle_id) {
291   // Remove non-configuration based bundles from content moderation based
292   // workflows when they are removed.
293   foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
294     if ($workflow->getTypePlugin()->appliesToEntityTypeAndBundle($entity_type_id, $bundle_id)) {
295       $workflow->getTypePlugin()->removeEntityTypeAndBundle($entity_type_id, $bundle_id);
296       $workflow->save();
297     }
298   }
299 }
300
301 /**
302  * Implements hook_ENTITY_TYPE_insert().
303  */
304 function content_moderation_workflow_insert(WorkflowInterface $entity) {
305   // Clear bundle cache so workflow gets added or removed from the bundle
306   // information.
307   \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
308   // Clear field cache so extra field is added or removed.
309   \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
310 }
311
312 /**
313  * Implements hook_ENTITY_TYPE_update().
314  */
315 function content_moderation_workflow_update(WorkflowInterface $entity) {
316   // Clear bundle cache so workflow gets added or removed from the bundle
317   // information.
318   \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
319   // Clear field cache so extra field is added or removed.
320   \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
321 }