Commit bc00f081 authored by catch's avatar catch

Issue #2725533 by timmillwood, alexpott, amateescu, webchick, dixon_,...

Issue #2725533 by timmillwood, alexpott, amateescu, webchick, dixon_, larowlan, dawehner, catch, Crell, Bojhan, jibran, Wim Leers, agentrickard, Berdir: Add experimental content_moderation module
parent e1ef487b
......@@ -307,6 +307,9 @@ Contact module
- Jibran Ijaz 'jibran' https://www.drupal.org/u/jibran
- Andrey Postnikov 'andypost' https://www.drupal.org/u/andypost
Content Moderation module
- Tim Millwood 'timmillwood' https://www.drupal.org/u/timmillwood
Content Translation module
- Francesco Placella 'plach' https://www.drupal.org/u/plach
......
......@@ -63,6 +63,7 @@
"drupal/config": "self.version",
"drupal/config_translation": "self.version",
"drupal/contact": "self.version",
"drupal/content_moderation": "self.version",
"drupal/content_translation": "self.version",
"drupal/contextual": "self.version",
"drupal/core-annotation": "self.version",
......
langcode: en
status: true
dependencies: { }
id: archived
label: Archived
published: false
default_revision: true
weight: -8
langcode: en
status: true
dependencies: { }
id: draft
label: Draft
published: false
default_revision: false
weight: -10
langcode: en
status: true
dependencies: { }
id: published
label: Published
published: true
default_revision: true
weight: -9
langcode: en
status: true
dependencies:
config:
- content_moderation.state.archived
- content_moderation.state.draft
id: archived_draft
label: 'Un-archive to Draft'
stateFrom: archived
stateTo: draft
weight: -5
langcode: en
status: true
dependencies:
config:
- content_moderation.state.archived
- content_moderation.state.published
id: archived_published
label: 'Un-archive'
stateFrom: archived
stateTo: published
weight: -4
langcode: en
status: true
dependencies:
config:
- content_moderation.state.draft
id: draft_draft
label: 'Create New Draft'
stateFrom: draft
stateTo: draft
weight: -10
langcode: en
status: true
dependencies:
config:
- content_moderation.state.draft
- content_moderation.state.published
id: draft_published
label: 'Publish'
stateFrom: draft
stateTo: published
weight: -9
langcode: en
status: true
dependencies:
config:
- content_moderation.state.archived
- content_moderation.state.published
id: published_archived
label: 'Archive'
stateFrom: published
stateTo: archived
weight: -6
langcode: en
status: true
dependencies:
config:
- content_moderation.state.draft
- content_moderation.state.published
id: published_draft
label: 'Create New Draft'
stateFrom: published
stateTo: draft
weight: -8
langcode: en
status: true
dependencies:
config:
- content_moderation.state.published
id: published_published
label: 'Publish'
stateFrom: published
stateTo: published
weight: -7
content_moderation.state.*:
type: config_entity
label: 'Moderation state config'
mapping:
id:
type: string
label: 'ID'
label:
type: label
label: 'Label'
published:
type: boolean
label: 'Is published'
default_revision:
type: boolean
label: 'Is default revision'
weight:
type: integer
label: 'Weight'
content_moderation.state_transition.*:
type: config_entity
label: 'Moderation state transition config'
mapping:
id:
type: string
label: 'ID'
label:
type: label
label: 'Label'
stateFrom:
type: string
label: 'From state'
stateTo:
type: string
label: 'To state'
weight:
type: integer
label: 'Weight'
node.type.*.third_party.content_moderation:
type: mapping
label: 'Enable moderation states for this node type'
mapping:
enabled:
type: boolean
label: 'Moderation states enabled'
allowed_moderation_states:
type: sequence
sequence:
type: string
label: 'Moderation state'
default_moderation_state:
type: string
label: 'Moderation state for new content'
block_content.type.*.third_party.content_moderation:
type: mapping
label: 'Enable moderation states for this block content type'
mapping:
enabled:
type: boolean
label: 'Moderation states enabled'
allowed_moderation_states:
type: sequence
sequence:
type: string
label: 'Moderation state'
default_moderation_state:
type: string
label: 'Moderation state for new block content'
views.filter.latest_revision:
type: views_filter
label: 'Latest revision'
mapping:
value:
type: string
label: 'Value'
name: 'Content Moderation'
type: module
description: 'Provides moderation states for content'
version: VERSION
core: 8.x
package: Core (Experimental)
configure: content_moderation.overview
entity-moderation-form:
version: VERSION
css:
layout:
css/entity-moderation-form.css: {}
entity.moderation_state.add_form:
route_name: 'entity.moderation_state.add_form'
title: 'Add Moderation state'
appears_on:
- entity.moderation_state.collection
entity.moderation_state_transition.add_form:
route_name: 'entity.moderation_state_transition.add_form'
title: 'Add Moderation state transition'
appears_on:
- entity.moderation_state_transition.collection
# Moderation state menu items definition
content_moderation.overview:
title: 'Content moderation'
route_name: content_moderation.overview
description: 'Configure states and transitions for entities.'
parent: system.admin_config_workflow
entity.moderation_state.collection:
title: 'Moderation states'
route_name: entity.moderation_state.collection
description: 'Administer moderation states.'
parent: content_moderation.overview
weight: 10
# Moderation state transition menu items definition
entity.moderation_state_transition.collection:
title: 'Moderation state transitions'
route_name: entity.moderation_state_transition.collection
description: 'Administer moderation states transitions.'
parent: content_moderation.overview
weight: 20
moderation_state.entities:
deriver: 'Drupal\content_moderation\Plugin\Derivative\DynamicLocalTasks'
weight: 100
<?php
/**
* @file
* Contains content_moderation.module.
*/
use Drupal\content_moderation\EntityOperations;
use Drupal\content_moderation\EntityTypeInfo;
use Drupal\content_moderation\ContentPreprocess;
use Drupal\content_moderation\Plugin\Action\ModerationOptOutPublishNode;
use Drupal\content_moderation\Plugin\Action\ModerationOptOutUnpublishNode;
use Drupal\content_moderation\Plugin\Menu\EditTab;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeInterface;
use Drupal\node\Plugin\Action\PublishNode;
use Drupal\node\Plugin\Action\UnpublishNode;
/**
* Implements hook_help().
*/
function content_moderation_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
// Main module help for the content_moderation module.
case 'help.page.content_moderation':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Content Moderation module provides basic moderation for content. 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']) . '</p>';
return $output;
}
}
/**
* Creates an EntityTypeInfo object to respond to entity hooks.
*
* @return \Drupal\content_moderation\EntityTypeInfo
*/
function _content_moderation_create_entity_type_info() {
return new EntityTypeInfo(
\Drupal::service('string_translation'),
\Drupal::service('content_moderation.moderation_information'),
\Drupal::service('entity_type.manager')
);
}
/**
* Implements hook_entity_base_field_info().
*/
function content_moderation_entity_base_field_info(EntityTypeInterface $entity_type) {
return _content_moderation_create_entity_type_info()->entityBaseFieldInfo($entity_type);
}
/**
* Implements hook_entity_type_alter().
*/
function content_moderation_entity_type_alter(array &$entity_types) {
_content_moderation_create_entity_type_info()->entityTypeAlter($entity_types);
}
/**
* Implements hook_entity_operation().
*/
function content_moderation_entity_operation(EntityInterface $entity) {
_content_moderation_create_entity_type_info()->entityOperation($entity);
}
/**
* Sets required flag based on enabled state.
*/
function content_moderation_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) {
_content_moderation_create_entity_type_info()->entityBundleFieldInfoAlter($fields, $entity_type, $bundle);
}
/**
* Creates an EntityOperations object to respond to entity operation hooks.
*
* @return \Drupal\content_moderation\EntityOperations
*/
function _content_moderation_create_entity_operations() {
return new EntityOperations(
\Drupal::service('content_moderation.moderation_information'),
\Drupal::service('entity_type.manager'),
\Drupal::service('form_builder'),
\Drupal::service('content_moderation.revision_tracker')
);
}
/**
* Implements hook_entity_presave().
*/
function content_moderation_entity_presave(EntityInterface $entity) {
return _content_moderation_create_entity_operations()->entityPresave($entity);
}
/**
* Implements hook_entity_insert().
*/
function content_moderation_entity_insert(EntityInterface $entity) {
return _content_moderation_create_entity_operations()->entityInsert($entity);
}
/**
* Implements hook_entity_update().
*/
function content_moderation_entity_update(EntityInterface $entity) {
return _content_moderation_create_entity_operations()->entityUpdate($entity);
}
/**
* Implements hook_local_tasks_alter().
*/
function content_moderation_local_tasks_alter(&$local_tasks) {
$content_entity_type_ids = array_keys(array_filter(\Drupal::entityTypeManager()->getDefinitions(), function (EntityTypeInterface $entity_type) {
return $entity_type->isRevisionable();
}));
foreach ($content_entity_type_ids as $content_entity_type_id) {
if (isset($local_tasks["entity.$content_entity_type_id.edit_form"])) {
$local_tasks["entity.$content_entity_type_id.edit_form"]['class'] = EditTab::class;
$local_tasks["entity.$content_entity_type_id.edit_form"]['entity_type_id'] = $content_entity_type_id;
}
}
}
/**
* Implements hook_form_alter().
*/
function content_moderation_form_alter(&$form, FormStateInterface $form_state, $form_id) {
_content_moderation_create_entity_type_info()->bundleFormAlter($form, $form_state, $form_id);
}
/**
* Implements hook_preprocess_HOOK().
*
* Many default node templates rely on $page to determine whether to output the
* node title as part of the node content.
*/
function content_moderation_preprocess_node(&$variables) {
$content_process = new ContentPreprocess(\Drupal::routeMatch());
$content_process->preprocessNode($variables);
}
/**
* Implements hook_entity_extra_field_info().
*/
function content_moderation_entity_extra_field_info() {
return _content_moderation_create_entity_type_info()->entityExtraFieldInfo();
}
/**
* Implements hook_entity_view().
*/
function content_moderation_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
_content_moderation_create_entity_operations()->entityView($build, $entity, $display, $view_mode);
}
/**
* Implements hook_node_access().
*
* Nodes in particular should be viewable if unpublished and the user has
* the appropriate permission. This permission is therefore effectively
* mandatory for any user that wants to moderate things.
*/
function content_moderation_node_access(NodeInterface $node, $operation, AccountInterface $account) {
/** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
$moderation_info = Drupal::service('content_moderation.moderation_information');
$access_result = NULL;
if ($operation === 'view') {
$access_result = (!$node->isPublished())
? AccessResult::allowedIfHasPermission($account, 'view any unpublished content')
: AccessResult::neutral();
$access_result->addCacheableDependency($node);
}
elseif ($operation === 'update' && $moderation_info->isModeratableEntity($node) && $node->moderation_state && $node->moderation_state->target_id) {
/** @var \Drupal\content_moderation\StateTransitionValidation $transition_validation */
$transition_validation = \Drupal::service('content_moderation.state_transition_validation');
$valid_transition_targets = $transition_validation->getValidTransitionTargets($node, $account);
$access_result = $valid_transition_targets ? AccessResult::neutral() : AccessResult::forbidden();
$access_result->addCacheableDependency($node);
$access_result->addCacheableDependency($account);
foreach ($valid_transition_targets as $valid_transition_target) {
$access_result->addCacheableDependency($valid_transition_target);
}
}
return $access_result;
}
/**
* Implements hook_theme().
*/
function content_moderation_theme() {
return ['entity_moderation_form' => ['render element' => 'form']];
}
/**
* Implements hook_action_info_alter().
*/
function content_moderation_action_info_alter(&$definitions) {
// The publish/unpublish actions are not valid on moderated entities. So swap
// their implementations out for alternates that will become a no-op on a
// moderated node. If another module has already swapped out those classes,
// though, we'll be polite and do nothing.
if (isset($definitions['node_publish_action']['class']) && $definitions['node_publish_action']['class'] == PublishNode::class) {
$definitions['node_publish_action']['class'] = ModerationOptOutPublishNode::class;
}
if (isset($definitions['node_unpublish_action']['class']) && $definitions['node_unpublish_action']['class'] == UnpublishNode::class) {
$definitions['node_unpublish_action']['class'] = ModerationOptOutUnpublishNode::class;
}
}
view any unpublished content:
title: 'View any unpublished content'
description: 'This permission is necessary for any users that may moderate content.'
'view moderation states':
title: 'View moderation states'
description: 'View moderation states.'
'administer moderation states':
title: 'Administer moderation states'
description: 'Create and edit moderation states.'
'restrict access': TRUE
'administer moderation state transitions':
title: 'Administer content moderation state transitions'
description: 'Create and edit content moderation state transitions.'
'restrict access': TRUE
view latest version:
title: 'View the latest version'
description: 'View the latest version of an entity. (Also requires "View any unpublished content" permission)'
permission_callbacks:
- \Drupal\content_moderation\Permissions::transitionPermissions
content_moderation.overview:
path: '/admin/config/workflow/moderation'
defaults:
_controller: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage'
_title: 'Content moderation'
requirements:
_permission: 'access administration pages'
# ModerationState routing definition
entity.moderation_state.collection:
path: '/admin/config/workflow/moderation/states'
defaults:
_entity_list: 'moderation_state'
_title: 'Moderation states'
requirements:
_permission: 'administer moderation states'
entity.moderation_state.add_form:
path: '/admin/config/workflow/moderation/states/add'
defaults:
_entity_form: 'moderation_state.add'
_title: 'Add Moderation state'
requirements:
_permission: 'administer moderation states'
entity.moderation_state.edit_form:
path: '/admin/config/workflow/moderation/states/{moderation_state}'
defaults:
_entity_form: 'moderation_state.edit'
_title: 'Edit Moderation state'
requirements:
_permission: 'administer moderation states'
entity.moderation_state.delete_form:
path: '/admin/config/workflow/moderation/states/{moderation_state}/delete'
defaults:
_entity_form: 'moderation_state.delete'
_title: 'Delete Moderation state'
requirements:
_permission: 'administer moderation states'
# ModerationStateTransition routing definition
entity.moderation_state_transition.collection:
path: '/admin/config/workflow/moderation/transitions'
defaults:
_entity_list: 'moderation_state_transition'
_title: 'Moderation state transitions'
requirements:
_permission: 'administer moderation state transitions'
entity.moderation_state_transition.add_form:
path: '/admin/config/workflow/moderation/transitions/add'
defaults:
_entity_form: 'moderation_state_transition.add'
_title: 'Add Moderation state transition'
requirements:
_permission: 'administer moderation state transitions'
entity.moderation_state_transition.edit_form:
path: '/admin/config/workflow/moderation/transitions/{moderation_state_transition}'
defaults:
_entity_form: 'moderation_state_transition.edit'
_title: 'Edit Moderation state transition'
requirements:
_permission: 'administer moderation state transitions'
entity.moderation_state_transition.delete_form:
path: '/admin/config/workflow/moderation/transitions/{moderation_state_transition}/delete'
defaults:
_entity_form: 'moderation_state_transition.delete'
_title: 'Delete Moderation state transition'
requirements:
_permission: 'administer moderation state transitions'
services:
paramconverter.latest_revision:
class: Drupal\content_moderation\ParamConverter\EntityRevisionConverter
arguments: ['@entity.manager', '@content_moderation.moderation_information']
tags:
- { name: paramconverter, priority: 5 }
content_moderation.state_transition_validation:
class: \Drupal\content_moderation\StateTransitionValidation
arguments: ['@entity_type.manager', '@entity.query']
content_moderation.moderation_information:
class: Drupal\content_moderation\ModerationInformation
arguments: ['@entity_type.manager', '@current_user']
access_check.latest_revision:
class: Drupal\content_moderation\Access\LatestRevisionCheck
arguments: ['@content_moderation.moderation_information']
tags:
- { name: access_check, applies_to: _content_moderation_latest_version }
content_moderation.revision_tracker:
class: Drupal\content_moderation\RevisionTracker
arguments: ['@database']
tags:
- { name: backend_overridable }
<?php
/**
* @file
* Provide views data for content_moderation.module.
*
* @ingroup views_module_handlers
*/
use Drupal\content_moderation\ViewsData;
/**
* Implements hook_views_data().
*/
function content_moderation_views_data() {
return _content_moderation_views_data_object()->getViewsData();
}
/**
* Implements hook_views_data_alter().
*/
function content_moderation_views_data_alter(array &$data) {
_content_moderation_views_data_object()->alterViewsData($data);
}
/**
* Creates a ViewsData object to respond to views hooks.
*
* @return \Drupal\content_moderation\ViewsData
* The content moderation ViewsData object.
*/
function _content_moderation_views_data_object() {
return new ViewsData(
\Drupal::service('entity_type.manager'),
\Drupal::service('content_moderation.moderation_information')
);
}
ul.entity-moderation-form {
list-style: none;