annotate core/modules/content_moderation/content_moderation.module @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 /**
Chris@0 4 * @file
Chris@0 5 * Contains content_moderation.module.
Chris@0 6 */
Chris@0 7
Chris@0 8 use Drupal\content_moderation\EntityOperations;
Chris@0 9 use Drupal\content_moderation\EntityTypeInfo;
Chris@0 10 use Drupal\content_moderation\ContentPreprocess;
Chris@14 11 use Drupal\content_moderation\Plugin\Action\ModerationOptOutPublish;
Chris@14 12 use Drupal\content_moderation\Plugin\Action\ModerationOptOutUnpublish;
Chris@0 13 use Drupal\Core\Access\AccessResult;
Chris@18 14 use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
Chris@0 15 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
Chris@0 16 use Drupal\Core\Entity\EntityInterface;
Chris@0 17 use Drupal\Core\Entity\EntityPublishedInterface;
Chris@0 18 use Drupal\Core\Entity\EntityTypeInterface;
Chris@0 19 use Drupal\Core\Field\FieldDefinitionInterface;
Chris@0 20 use Drupal\Core\Field\FieldItemListInterface;
Chris@0 21 use Drupal\Core\Form\FormStateInterface;
Chris@0 22 use Drupal\Core\Routing\RouteMatchInterface;
Chris@0 23 use Drupal\Core\Session\AccountInterface;
Chris@14 24 use Drupal\Core\Url;
Chris@18 25 use Drupal\views\Plugin\views\filter\Broken;
Chris@18 26 use Drupal\views\ViewExecutable;
Chris@18 27 use Drupal\views\Views;
Chris@0 28 use Drupal\workflows\WorkflowInterface;
Chris@14 29 use Drupal\Core\Action\Plugin\Action\PublishAction;
Chris@14 30 use Drupal\Core\Action\Plugin\Action\UnpublishAction;
Chris@0 31 use Drupal\workflows\Entity\Workflow;
Chris@14 32 use Drupal\views\Entity\View;
Chris@0 33
Chris@0 34 /**
Chris@0 35 * Implements hook_help().
Chris@0 36 */
Chris@0 37 function content_moderation_help($route_name, RouteMatchInterface $route_match) {
Chris@0 38 switch ($route_name) {
Chris@0 39 // Main module help for the content_moderation module.
Chris@0 40 case 'help.page.content_moderation':
Chris@0 41 $output = '';
Chris@0 42 $output .= '<h3>' . t('About') . '</h3>';
Chris@14 43 $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>';
Chris@0 44 $output .= '<h3>' . t('Uses') . '</h3>';
Chris@0 45 $output .= '<dl>';
Chris@14 46 $output .= '<dt>' . t('Applying workflows') . '</dt>';
Chris@14 47 $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>';
Chris@14 48 if (\Drupal::moduleHandler()->moduleExists('views')) {
Chris@14 49 $moderated_content_view = View::load('moderated_content');
Chris@14 50 if (isset($moderated_content_view) && $moderated_content_view->status() === TRUE) {
Chris@14 51 $output .= '<dt>' . t('Moderating content') . '</dt>';
Chris@14 52 $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>';
Chris@14 53 }
Chris@14 54 }
Chris@0 55 $output .= '<dt>' . t('Configure Content Moderation permissions') . '</dt>';
Chris@14 56 $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>';
Chris@0 57 $output .= '</dl>';
Chris@0 58 return $output;
Chris@0 59 }
Chris@0 60 }
Chris@0 61
Chris@0 62 /**
Chris@0 63 * Implements hook_entity_base_field_info().
Chris@0 64 */
Chris@0 65 function content_moderation_entity_base_field_info(EntityTypeInterface $entity_type) {
Chris@0 66 return \Drupal::service('class_resolver')
Chris@0 67 ->getInstanceFromDefinition(EntityTypeInfo::class)
Chris@0 68 ->entityBaseFieldInfo($entity_type);
Chris@0 69 }
Chris@0 70
Chris@0 71 /**
Chris@18 72 * Implements hook_entity_bundle_field_info().
Chris@18 73 */
Chris@18 74 function content_moderation_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
Chris@18 75 if (isset($base_field_definitions['moderation_state'])) {
Chris@18 76 // Add the target bundle to the moderation state field. Since each bundle
Chris@18 77 // can be attached to a different moderation workflow, adding this
Chris@18 78 // information to the field definition allows the associated workflow to be
Chris@18 79 // derived where a field definition is present.
Chris@18 80 $base_field_definitions['moderation_state']->setTargetBundle($bundle);
Chris@18 81 return [
Chris@18 82 'moderation_state' => $base_field_definitions['moderation_state'],
Chris@18 83 ];
Chris@18 84 }
Chris@18 85 }
Chris@18 86
Chris@18 87 /**
Chris@0 88 * Implements hook_entity_type_alter().
Chris@0 89 */
Chris@0 90 function content_moderation_entity_type_alter(array &$entity_types) {
Chris@0 91 \Drupal::service('class_resolver')
Chris@0 92 ->getInstanceFromDefinition(EntityTypeInfo::class)
Chris@0 93 ->entityTypeAlter($entity_types);
Chris@0 94 }
Chris@0 95
Chris@0 96 /**
Chris@0 97 * Implements hook_entity_presave().
Chris@0 98 */
Chris@0 99 function content_moderation_entity_presave(EntityInterface $entity) {
Chris@0 100 return \Drupal::service('class_resolver')
Chris@0 101 ->getInstanceFromDefinition(EntityOperations::class)
Chris@0 102 ->entityPresave($entity);
Chris@0 103 }
Chris@0 104
Chris@0 105 /**
Chris@0 106 * Implements hook_entity_insert().
Chris@0 107 */
Chris@0 108 function content_moderation_entity_insert(EntityInterface $entity) {
Chris@0 109 return \Drupal::service('class_resolver')
Chris@0 110 ->getInstanceFromDefinition(EntityOperations::class)
Chris@0 111 ->entityInsert($entity);
Chris@0 112 }
Chris@0 113
Chris@0 114 /**
Chris@0 115 * Implements hook_entity_update().
Chris@0 116 */
Chris@0 117 function content_moderation_entity_update(EntityInterface $entity) {
Chris@0 118 return \Drupal::service('class_resolver')
Chris@0 119 ->getInstanceFromDefinition(EntityOperations::class)
Chris@0 120 ->entityUpdate($entity);
Chris@0 121 }
Chris@0 122
Chris@0 123 /**
Chris@0 124 * Implements hook_entity_delete().
Chris@0 125 */
Chris@0 126 function content_moderation_entity_delete(EntityInterface $entity) {
Chris@0 127 return \Drupal::service('class_resolver')
Chris@0 128 ->getInstanceFromDefinition(EntityOperations::class)
Chris@0 129 ->entityDelete($entity);
Chris@0 130 }
Chris@0 131
Chris@0 132 /**
Chris@0 133 * Implements hook_entity_revision_delete().
Chris@0 134 */
Chris@0 135 function content_moderation_entity_revision_delete(EntityInterface $entity) {
Chris@0 136 return \Drupal::service('class_resolver')
Chris@0 137 ->getInstanceFromDefinition(EntityOperations::class)
Chris@0 138 ->entityRevisionDelete($entity);
Chris@0 139 }
Chris@0 140
Chris@0 141 /**
Chris@0 142 * Implements hook_entity_translation_delete().
Chris@0 143 */
Chris@0 144 function content_moderation_entity_translation_delete(EntityInterface $translation) {
Chris@0 145 return \Drupal::service('class_resolver')
Chris@0 146 ->getInstanceFromDefinition(EntityOperations::class)
Chris@0 147 ->entityTranslationDelete($translation);
Chris@0 148 }
Chris@0 149
Chris@0 150 /**
Chris@14 151 * Implements hook_entity_prepare_form().
Chris@14 152 */
Chris@14 153 function content_moderation_entity_prepare_form(EntityInterface $entity, $operation, FormStateInterface $form_state) {
Chris@14 154 \Drupal::service('class_resolver')
Chris@14 155 ->getInstanceFromDefinition(EntityTypeInfo::class)
Chris@14 156 ->entityPrepareForm($entity, $operation, $form_state);
Chris@14 157 }
Chris@14 158
Chris@14 159 /**
Chris@0 160 * Implements hook_form_alter().
Chris@0 161 */
Chris@0 162 function content_moderation_form_alter(&$form, FormStateInterface $form_state, $form_id) {
Chris@0 163 \Drupal::service('class_resolver')
Chris@0 164 ->getInstanceFromDefinition(EntityTypeInfo::class)
Chris@0 165 ->formAlter($form, $form_state, $form_id);
Chris@0 166 }
Chris@0 167
Chris@0 168 /**
Chris@0 169 * Implements hook_preprocess_HOOK().
Chris@0 170 */
Chris@0 171 function content_moderation_preprocess_node(&$variables) {
Chris@0 172 \Drupal::service('class_resolver')
Chris@0 173 ->getInstanceFromDefinition(ContentPreprocess::class)
Chris@0 174 ->preprocessNode($variables);
Chris@0 175 }
Chris@0 176
Chris@0 177 /**
Chris@0 178 * Implements hook_entity_extra_field_info().
Chris@0 179 */
Chris@0 180 function content_moderation_entity_extra_field_info() {
Chris@0 181 return \Drupal::service('class_resolver')
Chris@0 182 ->getInstanceFromDefinition(EntityTypeInfo::class)
Chris@0 183 ->entityExtraFieldInfo();
Chris@0 184 }
Chris@0 185
Chris@0 186 /**
Chris@0 187 * Implements hook_entity_view().
Chris@0 188 */
Chris@0 189 function content_moderation_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
Chris@0 190 \Drupal::service('class_resolver')
Chris@0 191 ->getInstanceFromDefinition(EntityOperations::class)
Chris@0 192 ->entityView($build, $entity, $display, $view_mode);
Chris@0 193 }
Chris@0 194
Chris@0 195 /**
Chris@18 196 * Implements hook_entity_form_display_alter().
Chris@18 197 */
Chris@18 198 function content_moderation_entity_form_display_alter(EntityFormDisplayInterface $form_display, array $context) {
Chris@18 199 if ($context['form_mode'] === 'layout_builder') {
Chris@18 200 $form_display->setComponent('moderation_state', [
Chris@18 201 'type' => 'moderation_state_default',
Chris@18 202 'weight' => -900,
Chris@18 203 'settings' => [],
Chris@18 204 ]);
Chris@18 205 }
Chris@18 206 }
Chris@18 207
Chris@18 208 /**
Chris@0 209 * Implements hook_entity_access().
Chris@0 210 *
Chris@0 211 * Entities should be viewable if unpublished and the user has the appropriate
Chris@0 212 * permission. This permission is therefore effectively mandatory for any user
Chris@0 213 * that wants to moderate things.
Chris@0 214 */
Chris@0 215 function content_moderation_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
Chris@0 216 /** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
Chris@0 217 $moderation_info = Drupal::service('content_moderation.moderation_information');
Chris@0 218
Chris@0 219 $access_result = NULL;
Chris@0 220 if ($operation === 'view') {
Chris@0 221 $access_result = (($entity instanceof EntityPublishedInterface) && !$entity->isPublished())
Chris@0 222 ? AccessResult::allowedIfHasPermission($account, 'view any unpublished content')
Chris@0 223 : AccessResult::neutral();
Chris@0 224
Chris@0 225 $access_result->addCacheableDependency($entity);
Chris@0 226 }
Chris@0 227 elseif ($operation === 'update' && $moderation_info->isModeratedEntity($entity) && $entity->moderation_state) {
Chris@0 228 /** @var \Drupal\content_moderation\StateTransitionValidation $transition_validation */
Chris@0 229 $transition_validation = \Drupal::service('content_moderation.state_transition_validation');
Chris@0 230
Chris@0 231 $valid_transition_targets = $transition_validation->getValidTransitions($entity, $account);
Chris@17 232 $access_result = $valid_transition_targets ? AccessResult::neutral() : AccessResult::forbidden('No valid transitions exist for given account.');
Chris@0 233
Chris@0 234 $access_result->addCacheableDependency($entity);
Chris@0 235 $access_result->addCacheableDependency($account);
Chris@0 236 $workflow = $moderation_info->getWorkflowForEntity($entity);
Chris@0 237 $access_result->addCacheableDependency($workflow);
Chris@0 238 foreach ($valid_transition_targets as $valid_transition_target) {
Chris@0 239 $access_result->addCacheableDependency($valid_transition_target);
Chris@0 240 }
Chris@0 241 }
Chris@0 242
Chris@18 243 // Do not allow users to delete the state that is configured as the default
Chris@18 244 // state for the workflow.
Chris@18 245 if ($entity instanceof WorkflowInterface) {
Chris@18 246 $configuration = $entity->getTypePlugin()->getConfiguration();
Chris@18 247 if (!empty($configuration['default_moderation_state']) && $operation === sprintf('delete-state:%s', $configuration['default_moderation_state'])) {
Chris@18 248 return AccessResult::forbidden()->addCacheableDependency($entity);
Chris@18 249 }
Chris@18 250 }
Chris@18 251
Chris@0 252 return $access_result;
Chris@0 253 }
Chris@0 254
Chris@0 255 /**
Chris@0 256 * Implements hook_entity_field_access().
Chris@0 257 */
Chris@0 258 function content_moderation_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
Chris@0 259 if ($items && $operation === 'edit') {
Chris@0 260 /** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
Chris@0 261 $moderation_info = Drupal::service('content_moderation.moderation_information');
Chris@0 262
Chris@0 263 $entity_type = \Drupal::entityTypeManager()->getDefinition($field_definition->getTargetEntityTypeId());
Chris@0 264
Chris@0 265 $entity = $items->getEntity();
Chris@0 266
Chris@0 267 // Deny edit access to the published field if the entity is being moderated.
Chris@0 268 if ($entity_type->hasKey('published') && $moderation_info->isModeratedEntity($entity) && $entity->moderation_state && $field_definition->getName() == $entity_type->getKey('published')) {
Chris@17 269 return AccessResult::forbidden('Cannot edit the published field of moderated entities.');
Chris@0 270 }
Chris@0 271 }
Chris@0 272
Chris@0 273 return AccessResult::neutral();
Chris@0 274 }
Chris@0 275
Chris@0 276 /**
Chris@0 277 * Implements hook_theme().
Chris@0 278 */
Chris@0 279 function content_moderation_theme() {
Chris@0 280 return ['entity_moderation_form' => ['render element' => 'form']];
Chris@0 281 }
Chris@0 282
Chris@0 283 /**
Chris@0 284 * Implements hook_action_info_alter().
Chris@0 285 */
Chris@0 286 function content_moderation_action_info_alter(&$definitions) {
Chris@0 287
Chris@0 288 // The publish/unpublish actions are not valid on moderated entities. So swap
Chris@0 289 // their implementations out for alternates that will become a no-op on a
Chris@14 290 // moderated entity. If another module has already swapped out those classes,
Chris@0 291 // though, we'll be polite and do nothing.
Chris@14 292 foreach ($definitions as &$definition) {
Chris@14 293 if ($definition['id'] === 'entity:publish_action' && $definition['class'] == PublishAction::class) {
Chris@14 294 $definition['class'] = ModerationOptOutPublish::class;
Chris@14 295 }
Chris@14 296 if ($definition['id'] === 'entity:unpublish_action' && $definition['class'] == UnpublishAction::class) {
Chris@14 297 $definition['class'] = ModerationOptOutUnpublish::class;
Chris@14 298 }
Chris@0 299 }
Chris@0 300 }
Chris@0 301
Chris@0 302 /**
Chris@0 303 * Implements hook_entity_bundle_info_alter().
Chris@0 304 */
Chris@0 305 function content_moderation_entity_bundle_info_alter(&$bundles) {
Chris@14 306 $translatable = FALSE;
Chris@0 307 /** @var \Drupal\workflows\WorkflowInterface $workflow */
Chris@0 308 foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
Chris@0 309 /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */
Chris@0 310 $plugin = $workflow->getTypePlugin();
Chris@0 311 foreach ($plugin->getEntityTypes() as $entity_type_id) {
Chris@0 312 foreach ($plugin->getBundlesForEntityType($entity_type_id) as $bundle_id) {
Chris@0 313 if (isset($bundles[$entity_type_id][$bundle_id])) {
Chris@0 314 $bundles[$entity_type_id][$bundle_id]['workflow'] = $workflow->id();
Chris@14 315 // If we have even one moderation-enabled translatable bundle, we need
Chris@14 316 // to make the moderation state bundle translatable as well, to enable
Chris@14 317 // the revision translation merge logic also for content moderation
Chris@14 318 // state revisions.
Chris@14 319 if (!empty($bundles[$entity_type_id][$bundle_id]['translatable'])) {
Chris@14 320 $translatable = TRUE;
Chris@14 321 }
Chris@0 322 }
Chris@0 323 }
Chris@0 324 }
Chris@0 325 }
Chris@14 326 $bundles['content_moderation_state']['content_moderation_state']['translatable'] = $translatable;
Chris@0 327 }
Chris@0 328
Chris@0 329 /**
Chris@0 330 * Implements hook_entity_bundle_delete().
Chris@0 331 */
Chris@0 332 function content_moderation_entity_bundle_delete($entity_type_id, $bundle_id) {
Chris@0 333 // Remove non-configuration based bundles from content moderation based
Chris@0 334 // workflows when they are removed.
Chris@0 335 foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
Chris@0 336 if ($workflow->getTypePlugin()->appliesToEntityTypeAndBundle($entity_type_id, $bundle_id)) {
Chris@0 337 $workflow->getTypePlugin()->removeEntityTypeAndBundle($entity_type_id, $bundle_id);
Chris@0 338 $workflow->save();
Chris@0 339 }
Chris@0 340 }
Chris@0 341 }
Chris@0 342
Chris@0 343 /**
Chris@0 344 * Implements hook_ENTITY_TYPE_insert().
Chris@0 345 */
Chris@0 346 function content_moderation_workflow_insert(WorkflowInterface $entity) {
Chris@0 347 // Clear bundle cache so workflow gets added or removed from the bundle
Chris@0 348 // information.
Chris@0 349 \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
Chris@0 350 // Clear field cache so extra field is added or removed.
Chris@0 351 \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
Chris@18 352 // Clear the views data cache so the extra field is available in views.
Chris@18 353 if (\Drupal::moduleHandler()->moduleExists('views')) {
Chris@18 354 Views::viewsData()->clear();
Chris@18 355 }
Chris@0 356 }
Chris@0 357
Chris@0 358 /**
Chris@0 359 * Implements hook_ENTITY_TYPE_update().
Chris@0 360 */
Chris@0 361 function content_moderation_workflow_update(WorkflowInterface $entity) {
Chris@0 362 // Clear bundle cache so workflow gets added or removed from the bundle
Chris@0 363 // information.
Chris@0 364 \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
Chris@0 365 // Clear field cache so extra field is added or removed.
Chris@0 366 \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
Chris@18 367 // Clear the views data cache so the extra field is available in views.
Chris@18 368 if (\Drupal::moduleHandler()->moduleExists('views')) {
Chris@18 369 Views::viewsData()->clear();
Chris@18 370 }
Chris@0 371 }
Chris@18 372
Chris@18 373 /**
Chris@18 374 * Implements hook_views_post_execute().
Chris@18 375 */
Chris@18 376 function content_moderation_views_post_execute(ViewExecutable $view) {
Chris@18 377 // @todo, remove this once broken handlers in views configuration result in
Chris@18 378 // a view no longer returning results. https://www.drupal.org/node/2907954.
Chris@18 379 foreach ($view->filter as $id => $filter) {
Chris@18 380 if (strpos($id, 'moderation_state') === 0 && $filter instanceof Broken) {
Chris@18 381 $view->result = [];
Chris@18 382 break;
Chris@18 383 }
Chris@18 384 }
Chris@18 385 }