content_moderation.module 13 KB

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