Move into nested docroot
This commit is contained in:
parent
83a0d3a149
commit
c8b70abde9
13405 changed files with 0 additions and 0 deletions
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Access;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Routing\Access\AccessInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
* Access check for the entity moderation tab.
|
||||
*/
|
||||
class LatestRevisionCheck implements AccessInterface {
|
||||
|
||||
/**
|
||||
* The moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* Constructs a new LatestRevisionCheck.
|
||||
*
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
|
||||
* The moderation information service.
|
||||
*/
|
||||
public function __construct(ModerationInformationInterface $moderation_information) {
|
||||
$this->moderationInfo = $moderation_information;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that there is a forward revision available.
|
||||
*
|
||||
* This checker assumes the presence of an '_entity_access' requirement key
|
||||
* in the same form as used by EntityAccessCheck.
|
||||
*
|
||||
* @param \Symfony\Component\Routing\Route $route
|
||||
* The route to check against.
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The parametrized route.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*
|
||||
* @see \Drupal\Core\Entity\EntityAccessCheck
|
||||
*/
|
||||
public function access(Route $route, RouteMatchInterface $route_match) {
|
||||
// This tab should not show up unless there's a reason to show it.
|
||||
$entity = $this->loadEntity($route, $route_match);
|
||||
return $this->moderationInfo->hasForwardRevision($entity)
|
||||
? AccessResult::allowed()->addCacheableDependency($entity)
|
||||
: AccessResult::forbidden()->addCacheableDependency($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default revision of the entity this route is for.
|
||||
*
|
||||
* @param \Symfony\Component\Routing\Route $route
|
||||
* The route to check against.
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The parametrized route.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\ContentEntityInterface
|
||||
* returns the Entity in question.
|
||||
*
|
||||
* @throws \Exception
|
||||
* A generic exception is thrown if the entity couldn't be loaded. This
|
||||
* almost always implies a developer error, so it should get turned into
|
||||
* an HTTP 500.
|
||||
*/
|
||||
protected function loadEntity(Route $route, RouteMatchInterface $route_match) {
|
||||
$entity_type = $route->getOption('_content_moderation_entity_type');
|
||||
|
||||
if ($entity = $route_match->getParameter($entity_type)) {
|
||||
if ($entity instanceof EntityInterface) {
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
throw new \Exception(sprintf('%s is not a valid entity route. The LatestRevisionCheck access checker may only be used with a route that has a single entity parameter.', $route_match->getRouteName()));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\user\EntityOwnerInterface;
|
||||
|
||||
/**
|
||||
* An interface for Content moderation state entity.
|
||||
*
|
||||
* Content moderation state entities track the moderation state of other content
|
||||
* entities.
|
||||
*/
|
||||
interface ContentModerationStateInterface extends ContentEntityInterface, EntityOwnerInterface {
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityTypeInterface;
|
||||
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
|
||||
|
||||
/**
|
||||
* Defines the content moderation state schema handler.
|
||||
*/
|
||||
class ContentModerationStateStorageSchema extends SqlContentEntityStorageSchema {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
|
||||
$schema = parent::getEntitySchema($entity_type, $reset);
|
||||
|
||||
// Creates an index to ensure that the lookup in
|
||||
// \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList::getModerationState()
|
||||
// is performant.
|
||||
$schema['content_moderation_state_field_data']['indexes'] += array(
|
||||
'content_moderation_state__lookup' => array('content_entity_type_id', 'content_entity_id', 'content_entity_revision_id'),
|
||||
);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Determines whether a route is the "Latest version" tab of a node.
|
||||
*/
|
||||
class ContentPreprocess implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The route match service.
|
||||
*
|
||||
* @var \Drupal\Core\Routing\RouteMatchInterface $routeMatch
|
||||
*/
|
||||
protected $routeMatch;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* Current route match service.
|
||||
*/
|
||||
public function __construct(RouteMatchInterface $route_match) {
|
||||
$this->routeMatch = $route_match;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('current_route_match')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for hook_preprocess_HOOK().
|
||||
*
|
||||
* @param array $variables
|
||||
* Theme variables to preprocess.
|
||||
*/
|
||||
public function preprocessNode(array &$variables) {
|
||||
// Set the 'page' template variable when the node is being displayed on the
|
||||
// "Latest version" tab provided by content_moderation.
|
||||
$variables['page'] = $variables['page'] || $this->isLatestVersionPage($variables['node']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a route is the "Latest version" tab of a node.
|
||||
*
|
||||
* @param \Drupal\node\Entity\Node $node
|
||||
* A node.
|
||||
*
|
||||
* @return bool
|
||||
* True if the current route is the latest version tab of the given node.
|
||||
*/
|
||||
public function isLatestVersionPage(Node $node) {
|
||||
return $this->routeMatch->getRouteName() == 'entity.node.latest_version'
|
||||
&& ($pageNode = $this->routeMatch->getParameter('node'))
|
||||
&& $pageNode->id() == $node->id();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity;
|
||||
|
||||
use Drupal\content_moderation\ContentModerationStateInterface;
|
||||
use Drupal\Core\Entity\ContentEntityBase;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\TypedData\TranslatableInterface;
|
||||
use Drupal\user\UserInterface;
|
||||
|
||||
/**
|
||||
* Defines the Content moderation state entity.
|
||||
*
|
||||
* @ContentEntityType(
|
||||
* id = "content_moderation_state",
|
||||
* label = @Translation("Content moderation state"),
|
||||
* label_singular = @Translation("content moderation state"),
|
||||
* label_plural = @Translation("content moderation states"),
|
||||
* label_count = @PluralTranslation(
|
||||
* singular = "@count content moderation state",
|
||||
* plural = "@count content moderation states"
|
||||
* ),
|
||||
* handlers = {
|
||||
* "storage_schema" = "Drupal\content_moderation\ContentModerationStateStorageSchema",
|
||||
* "views_data" = "\Drupal\views\EntityViewsData",
|
||||
* },
|
||||
* base_table = "content_moderation_state",
|
||||
* revision_table = "content_moderation_state_revision",
|
||||
* data_table = "content_moderation_state_field_data",
|
||||
* revision_data_table = "content_moderation_state_field_revision",
|
||||
* translatable = TRUE,
|
||||
* entity_keys = {
|
||||
* "id" = "id",
|
||||
* "revision" = "revision_id",
|
||||
* "uuid" = "uuid",
|
||||
* "uid" = "uid",
|
||||
* "langcode" = "langcode",
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class ContentModerationState extends ContentEntityBase implements ContentModerationStateInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
|
||||
$fields = parent::baseFieldDefinitions($entity_type);
|
||||
|
||||
$fields['uid'] = BaseFieldDefinition::create('entity_reference')
|
||||
->setLabel(t('User'))
|
||||
->setDescription(t('The username of the entity creator.'))
|
||||
->setSetting('target_type', 'user')
|
||||
->setDefaultValueCallback('Drupal\content_moderation\Entity\ContentModerationState::getCurrentUserId')
|
||||
->setTranslatable(TRUE)
|
||||
->setRevisionable(TRUE);
|
||||
|
||||
$fields['moderation_state'] = BaseFieldDefinition::create('entity_reference')
|
||||
->setLabel(t('Moderation state'))
|
||||
->setDescription(t('The moderation state of the referenced content.'))
|
||||
->setSetting('target_type', 'moderation_state')
|
||||
->setRequired(TRUE)
|
||||
->setTranslatable(TRUE)
|
||||
->setRevisionable(TRUE)
|
||||
->addConstraint('ModerationState', []);
|
||||
|
||||
$fields['content_entity_type_id'] = BaseFieldDefinition::create('string')
|
||||
->setLabel(t('Content entity type ID'))
|
||||
->setDescription(t('The ID of the content entity type this moderation state is for.'))
|
||||
->setRequired(TRUE)
|
||||
->setRevisionable(TRUE);
|
||||
|
||||
$fields['content_entity_id'] = BaseFieldDefinition::create('integer')
|
||||
->setLabel(t('Content entity ID'))
|
||||
->setDescription(t('The ID of the content entity this moderation state is for.'))
|
||||
->setRequired(TRUE)
|
||||
->setRevisionable(TRUE);
|
||||
|
||||
// @todo https://www.drupal.org/node/2779931 Add constraint that enforces
|
||||
// unique content_entity_type_id, content_entity_id and
|
||||
// content_entity_revision_id.
|
||||
|
||||
$fields['content_entity_revision_id'] = BaseFieldDefinition::create('integer')
|
||||
->setLabel(t('Content entity revision ID'))
|
||||
->setDescription(t('The revision ID of the content entity this moderation state is for.'))
|
||||
->setRequired(TRUE)
|
||||
->setRevisionable(TRUE);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getOwner() {
|
||||
return $this->get('uid')->entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getOwnerId() {
|
||||
return $this->getEntityKey('uid');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setOwnerId($uid) {
|
||||
$this->set('uid', $uid);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setOwner(UserInterface $account) {
|
||||
$this->set('uid', $account->id());
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or updates an entity's moderation state whilst saving that entity.
|
||||
*
|
||||
* @param \Drupal\content_moderation\Entity\ContentModerationState $content_moderation_state
|
||||
* The content moderation entity content entity to create or save.
|
||||
*
|
||||
* @internal
|
||||
* This method should only be called as a result of saving the related
|
||||
* content entity.
|
||||
*/
|
||||
public static function updateOrCreateFromEntity(ContentModerationState $content_moderation_state) {
|
||||
$content_moderation_state->realSave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default value callback for the 'uid' base field definition.
|
||||
*
|
||||
* @see \Drupal\content_moderation\Entity\ContentModerationState::baseFieldDefinitions()
|
||||
*
|
||||
* @return array
|
||||
* An array of default values.
|
||||
*/
|
||||
public static function getCurrentUserId() {
|
||||
return array(\Drupal::currentUser()->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save() {
|
||||
$related_entity = \Drupal::entityTypeManager()
|
||||
->getStorage($this->content_entity_type_id->value)
|
||||
->loadRevision($this->content_entity_revision_id->value);
|
||||
if ($related_entity instanceof TranslatableInterface) {
|
||||
$related_entity = $related_entity->getTranslation($this->activeLangcode);
|
||||
}
|
||||
$related_entity->moderation_state->target_id = $this->moderation_state->target_id;
|
||||
return $related_entity->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves an entity permanently.
|
||||
*
|
||||
* When saving existing entities, the entity is assumed to be complete,
|
||||
* partial updates of entities are not supported.
|
||||
*
|
||||
* @return int
|
||||
* Either SAVED_NEW or SAVED_UPDATED, depending on the operation performed.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
* In case of failures an exception is thrown.
|
||||
*/
|
||||
protected function realSave() {
|
||||
return parent::save();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity\Handler;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Customizations for block content entities.
|
||||
*/
|
||||
class BlockContentModerationHandler extends ModerationHandler {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enforceRevisionsEntityFormAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
$form['revision_information']['revision']['#default_value'] = TRUE;
|
||||
$form['revision_information']['revision']['#disabled'] = TRUE;
|
||||
$form['revision_information']['revision']['#description'] = $this->t('Revisions must be required when moderation is enabled.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enforceRevisionsBundleFormAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
$form['revision']['#default_value'] = 1;
|
||||
$form['revision']['#disabled'] = TRUE;
|
||||
$form['revision']['#description'] = $this->t('Revisions must be required when moderation is enabled.');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity\Handler;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityHandlerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Common customizations for most/all entities.
|
||||
*
|
||||
* This class is intended primarily as a base class.
|
||||
*/
|
||||
class ModerationHandler implements ModerationHandlerInterface, EntityHandlerInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
return new static();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onPresave(ContentEntityInterface $entity, $default_revision, $published_state) {
|
||||
// This is probably not necessary if configuration is setup correctly.
|
||||
$entity->setNewRevision(TRUE);
|
||||
$entity->isDefaultRevision($default_revision);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onBundleModerationConfigurationFormSubmit(ConfigEntityInterface $bundle) {
|
||||
// The Revisions portion of Entity API is not uniformly applied or
|
||||
// consistent. Until that's fixed, we'll make a best-attempt to apply it to
|
||||
// the common entity patterns so as to avoid every entity type needing to
|
||||
// implement this method, although some will still need to do so for now.
|
||||
// This is the API that should be universal, but isn't yet.
|
||||
// @see \Drupal\node\Entity\NodeType
|
||||
if (method_exists($bundle, 'setNewRevision')) {
|
||||
$bundle->setNewRevision(TRUE);
|
||||
}
|
||||
// This is the raw property used by NodeType, and likely others.
|
||||
elseif ($bundle->get('new_revision') !== NULL) {
|
||||
$bundle->set('new_revision', TRUE);
|
||||
}
|
||||
// This is the raw property used by BlockContentType, and maybe others.
|
||||
elseif ($bundle->get('revision') !== NULL) {
|
||||
$bundle->set('revision', TRUE);
|
||||
}
|
||||
|
||||
$bundle->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enforceRevisionsEntityFormAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enforceRevisionsBundleFormAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity\Handler;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Defines operations that need to vary by entity type.
|
||||
*
|
||||
* Much of the logic contained in this handler is an indication of flaws
|
||||
* in the Entity API that are insufficiently standardized between entity types.
|
||||
* Hopefully over time functionality can be removed from this interface.
|
||||
*/
|
||||
interface ModerationHandlerInterface {
|
||||
|
||||
/**
|
||||
* Operates on moderated content entities preSave().
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity to modify.
|
||||
* @param bool $default_revision
|
||||
* Whether the new revision should be made the default revision.
|
||||
* @param bool $published_state
|
||||
* Whether the state being transitioned to is a published state or not.
|
||||
*/
|
||||
public function onPresave(ContentEntityInterface $entity, $default_revision, $published_state);
|
||||
|
||||
/**
|
||||
* Operates on the bundle definition that has been marked as moderated.
|
||||
*
|
||||
* Note: The values on the EntityModerationForm itself are already saved
|
||||
* so do not need to be saved here. If any changes are made to the bundle
|
||||
* object here it is this method's responsibility to call save() on it.
|
||||
*
|
||||
* The most common use case is to force revisions on for this bundle if
|
||||
* moderation is enabled. That, sadly, does not have a common API in core.
|
||||
*
|
||||
* @param \Drupal\Core\Config\Entity\ConfigEntityInterface $bundle
|
||||
* The bundle definition that is being saved.
|
||||
*/
|
||||
public function onBundleModerationConfigurationFormSubmit(ConfigEntityInterface $bundle);
|
||||
|
||||
/**
|
||||
* Alters entity forms to enforce revision handling.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param string $form_id
|
||||
* The form id.
|
||||
*
|
||||
* @see hook_form_alter()
|
||||
*/
|
||||
public function enforceRevisionsEntityFormAlter(array &$form, FormStateInterface $form_state, $form_id);
|
||||
|
||||
/**
|
||||
* Alters bundle forms to enforce revision handling.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param string $form_id
|
||||
* The form id.
|
||||
*
|
||||
* @see hook_form_alter()
|
||||
*/
|
||||
public function enforceRevisionsBundleFormAlter(array &$form, FormStateInterface $form_state, $form_id);
|
||||
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity\Handler;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Customizations for node entities.
|
||||
*/
|
||||
class NodeModerationHandler extends ModerationHandler {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onPresave(ContentEntityInterface $entity, $default_revision, $published_state) {
|
||||
if ($this->shouldModerate($entity, $published_state)) {
|
||||
parent::onPresave($entity, $default_revision, $published_state);
|
||||
// Only nodes have a concept of published.
|
||||
/** @var \Drupal\node\NodeInterface $entity */
|
||||
$entity->setPublished($published_state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enforceRevisionsEntityFormAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
$form['revision']['#disabled'] = TRUE;
|
||||
$form['revision']['#default_value'] = TRUE;
|
||||
$form['revision']['#description'] = $this->t('Revisions are required.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enforceRevisionsBundleFormAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
/* @var \Drupal\node\Entity\NodeType $entity */
|
||||
$entity = $form_state->getFormObject()->getEntity();
|
||||
|
||||
if ($entity->getThirdPartySetting('content_moderation', 'enabled', FALSE)) {
|
||||
// Force the revision checkbox on.
|
||||
$form['workflow']['options']['#default_value']['revision'] = 'revision';
|
||||
$form['workflow']['options']['revision']['#disabled'] = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity's default revision and/or state needs adjusting.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity to check.
|
||||
* @param bool $published_state
|
||||
* Whether the state being transitioned to is a published state or not.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE when either the default revision or the state needs to be updated.
|
||||
*/
|
||||
protected function shouldModerate(ContentEntityInterface $entity, $published_state) {
|
||||
// @todo clarify the first condition.
|
||||
// First condition is needed so you can add a translation.
|
||||
// Second condition checks to see if the published status has changed.
|
||||
return $entity->isDefaultTranslation() || $entity->isPublished() !== $published_state;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityBase;
|
||||
use Drupal\content_moderation\ModerationStateInterface;
|
||||
|
||||
/**
|
||||
* Defines the Moderation state entity.
|
||||
*
|
||||
* @ConfigEntityType(
|
||||
* id = "moderation_state",
|
||||
* label = @Translation("Moderation state"),
|
||||
* handlers = {
|
||||
* "access" = "Drupal\content_moderation\ModerationStateAccessControlHandler",
|
||||
* "list_builder" = "Drupal\content_moderation\ModerationStateListBuilder",
|
||||
* "form" = {
|
||||
* "add" = "Drupal\content_moderation\Form\ModerationStateForm",
|
||||
* "edit" = "Drupal\content_moderation\Form\ModerationStateForm",
|
||||
* "delete" = "Drupal\content_moderation\Form\ModerationStateDeleteForm"
|
||||
* },
|
||||
* "route_provider" = {
|
||||
* "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
|
||||
* },
|
||||
* },
|
||||
* config_prefix = "state",
|
||||
* admin_permission = "administer moderation states",
|
||||
* entity_keys = {
|
||||
* "id" = "id",
|
||||
* "label" = "label",
|
||||
* "uuid" = "uuid",
|
||||
* "weight" = "weight",
|
||||
* },
|
||||
* links = {
|
||||
* "add-form" = "/admin/config/workflow/moderation/states/add",
|
||||
* "edit-form" = "/admin/config/workflow/moderation/states/{moderation_state}",
|
||||
* "delete-form" = "/admin/config/workflow/moderation/states/{moderation_state}/delete",
|
||||
* "collection" = "/admin/config/workflow/moderation/states"
|
||||
* },
|
||||
* config_export = {
|
||||
* "id",
|
||||
* "label",
|
||||
* "published",
|
||||
* "default_revision",
|
||||
* "weight",
|
||||
* },
|
||||
* )
|
||||
*/
|
||||
class ModerationState extends ConfigEntityBase implements ModerationStateInterface {
|
||||
|
||||
/**
|
||||
* The Moderation state ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* The Moderation state label.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $label;
|
||||
|
||||
/**
|
||||
* Whether this state represents a published node.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $published;
|
||||
|
||||
/**
|
||||
* Relative weight of this state.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $weight;
|
||||
|
||||
/**
|
||||
* Whether this state represents a default revision of the node.
|
||||
*
|
||||
* If this is a published state, then this property is ignored.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $default_revision;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isPublishedState() {
|
||||
return $this->published;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isDefaultRevisionState() {
|
||||
return $this->published || $this->default_revision;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityBase;
|
||||
use Drupal\content_moderation\ModerationStateTransitionInterface;
|
||||
|
||||
/**
|
||||
* Defines the Moderation state transition entity.
|
||||
*
|
||||
* @ConfigEntityType(
|
||||
* id = "moderation_state_transition",
|
||||
* label = @Translation("Moderation state transition"),
|
||||
* handlers = {
|
||||
* "list_builder" = "Drupal\content_moderation\ModerationStateTransitionListBuilder",
|
||||
* "form" = {
|
||||
* "add" = "Drupal\content_moderation\Form\ModerationStateTransitionForm",
|
||||
* "edit" = "Drupal\content_moderation\Form\ModerationStateTransitionForm",
|
||||
* "delete" = "Drupal\content_moderation\Form\ModerationStateTransitionDeleteForm"
|
||||
* },
|
||||
* "route_provider" = {
|
||||
* "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
|
||||
* },
|
||||
* },
|
||||
* config_prefix = "state_transition",
|
||||
* admin_permission = "administer moderation state transitions",
|
||||
* entity_keys = {
|
||||
* "id" = "id",
|
||||
* "label" = "label",
|
||||
* "uuid" = "uuid",
|
||||
* "weight" = "weight"
|
||||
* },
|
||||
* links = {
|
||||
* "add-form" = "/admin/config/workflow/moderation/transitions/add",
|
||||
* "edit-form" = "/admin/config/workflow/moderation/transitions/{moderation_state_transition}",
|
||||
* "delete-form" = "/admin/config/workflow/moderation/transitions/{moderation_state_transition}/delete",
|
||||
* "collection" = "/admin/config/workflow/moderation/transitions"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class ModerationStateTransition extends ConfigEntityBase implements ModerationStateTransitionInterface {
|
||||
|
||||
/**
|
||||
* The Moderation state transition ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* The Moderation state transition label.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $label;
|
||||
|
||||
/**
|
||||
* ID of from state.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $stateFrom;
|
||||
|
||||
/**
|
||||
* ID of to state.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $stateTo;
|
||||
|
||||
/**
|
||||
* Relative weight of this transition.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $weight;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function calculateDependencies() {
|
||||
parent::calculateDependencies();
|
||||
|
||||
if ($this->stateFrom) {
|
||||
$this->addDependency('config', ModerationState::load($this->stateFrom)->getConfigDependencyName());
|
||||
}
|
||||
if ($this->stateTo) {
|
||||
$this->addDependency('config', ModerationState::load($this->stateTo)->getConfigDependencyName());
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFromState() {
|
||||
return $this->stateFrom;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getToState() {
|
||||
return $this->stateTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getWeight() {
|
||||
return $this->weight;
|
||||
}
|
||||
|
||||
}
|
266
web/core/modules/content_moderation/src/EntityOperations.php
Normal file
266
web/core/modules/content_moderation/src/EntityOperations.php
Normal file
|
@ -0,0 +1,266 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\content_moderation\Entity\ContentModerationState;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormBuilderInterface;
|
||||
use Drupal\Core\TypedData\TranslatableInterface;
|
||||
use Drupal\content_moderation\Form\EntityModerationForm;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines a class for reacting to entity events.
|
||||
*/
|
||||
class EntityOperations implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The Moderation Information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* The Entity Type Manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The Form Builder service.
|
||||
*
|
||||
* @var \Drupal\Core\Form\FormBuilderInterface
|
||||
*/
|
||||
protected $formBuilder;
|
||||
|
||||
/**
|
||||
* The Revision Tracker service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\RevisionTrackerInterface
|
||||
*/
|
||||
protected $tracker;
|
||||
|
||||
/**
|
||||
* Constructs a new EntityOperations object.
|
||||
*
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
|
||||
* Moderation information service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* Entity type manager service.
|
||||
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
|
||||
* The form builder.
|
||||
* @param \Drupal\content_moderation\RevisionTrackerInterface $tracker
|
||||
* The revision tracker.
|
||||
*/
|
||||
public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, RevisionTrackerInterface $tracker) {
|
||||
$this->moderationInfo = $moderation_info;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->formBuilder = $form_builder;
|
||||
$this->tracker = $tracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('content_moderation.moderation_information'),
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('form_builder'),
|
||||
$container->get('content_moderation.revision_tracker')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acts on an entity and set published status based on the moderation state.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being saved.
|
||||
*/
|
||||
public function entityPresave(EntityInterface $entity) {
|
||||
if (!$this->moderationInfo->isModeratedEntity($entity)) {
|
||||
return;
|
||||
}
|
||||
if ($entity->moderation_state->target_id) {
|
||||
$moderation_state = $this->entityTypeManager
|
||||
->getStorage('moderation_state')
|
||||
->load($entity->moderation_state->target_id);
|
||||
$published_state = $moderation_state->isPublishedState();
|
||||
|
||||
// This entity is default if it is new, the default revision, or the
|
||||
// default revision is not published.
|
||||
$update_default_revision = $entity->isNew()
|
||||
|| $moderation_state->isDefaultRevisionState()
|
||||
|| !$this->isDefaultRevisionPublished($entity);
|
||||
|
||||
// Fire per-entity-type logic for handling the save process.
|
||||
$this->entityTypeManager->getHandler($entity->getEntityTypeId(), 'moderation')->onPresave($entity, $update_default_revision, $published_state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook bridge.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity that was just saved.
|
||||
*
|
||||
* @see hook_entity_insert()
|
||||
*/
|
||||
public function entityInsert(EntityInterface $entity) {
|
||||
if ($this->moderationInfo->isModeratedEntity($entity)) {
|
||||
$this->updateOrCreateFromEntity($entity);
|
||||
$this->setLatestRevision($entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook bridge.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity that was just saved.
|
||||
*
|
||||
* @see hook_entity_update()
|
||||
*/
|
||||
public function entityUpdate(EntityInterface $entity) {
|
||||
if ($this->moderationInfo->isModeratedEntity($entity)) {
|
||||
$this->updateOrCreateFromEntity($entity);
|
||||
$this->setLatestRevision($entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or updates the moderation state of an entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity to update or create a moderation state for.
|
||||
*/
|
||||
protected function updateOrCreateFromEntity(EntityInterface $entity) {
|
||||
$moderation_state = $entity->moderation_state->target_id;
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
if (!$moderation_state) {
|
||||
$moderation_state = $this->entityTypeManager
|
||||
->getStorage($entity->getEntityType()->getBundleEntityType())->load($entity->bundle())
|
||||
->getThirdPartySetting('content_moderation', 'default_moderation_state');
|
||||
}
|
||||
|
||||
// @todo what if $entity->moderation_state->target_id is null at this point?
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
$entity_id = $entity->id();
|
||||
$entity_revision_id = $entity->getRevisionId();
|
||||
|
||||
$storage = $this->entityTypeManager->getStorage('content_moderation_state');
|
||||
$entities = $storage->loadByProperties([
|
||||
'content_entity_type_id' => $entity_type_id,
|
||||
'content_entity_id' => $entity_id,
|
||||
]);
|
||||
|
||||
/** @var \Drupal\content_moderation\ContentModerationStateInterface $content_moderation_state */
|
||||
$content_moderation_state = reset($entities);
|
||||
if (!($content_moderation_state instanceof ContentModerationStateInterface)) {
|
||||
$content_moderation_state = $storage->create([
|
||||
'content_entity_type_id' => $entity_type_id,
|
||||
'content_entity_id' => $entity_id,
|
||||
]);
|
||||
}
|
||||
else {
|
||||
// Create a new revision.
|
||||
$content_moderation_state->setNewRevision(TRUE);
|
||||
}
|
||||
|
||||
// Sync translations.
|
||||
if ($entity->getEntityType()->hasKey('langcode')) {
|
||||
$entity_langcode = $entity->language()->getId();
|
||||
if (!$content_moderation_state->hasTranslation($entity_langcode)) {
|
||||
$content_moderation_state->addTranslation($entity_langcode);
|
||||
}
|
||||
if ($content_moderation_state->language()->getId() !== $entity_langcode) {
|
||||
$content_moderation_state = $content_moderation_state->getTranslation($entity_langcode);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the ContentModerationState entity for the inserted entity.
|
||||
$content_moderation_state->set('content_entity_revision_id', $entity_revision_id);
|
||||
$content_moderation_state->set('moderation_state', $moderation_state);
|
||||
ContentModerationState::updateOrCreateFromEntity($content_moderation_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the latest revision.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The content entity to create content_moderation_state entity for.
|
||||
*/
|
||||
protected function setLatestRevision(EntityInterface $entity) {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$this->tracker->setLatestRevision(
|
||||
$entity->getEntityTypeId(),
|
||||
$entity->id(),
|
||||
$entity->language()->getId(),
|
||||
$entity->getRevisionId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Act on entities being assembled before rendering.
|
||||
*
|
||||
* This is a hook bridge.
|
||||
*
|
||||
* @see hook_entity_view()
|
||||
* @see EntityFieldManagerInterface::getExtraFields()
|
||||
*/
|
||||
public function entityView(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
|
||||
if (!$this->moderationInfo->isModeratedEntity($entity)) {
|
||||
return;
|
||||
}
|
||||
if (!$this->moderationInfo->isLatestRevision($entity)) {
|
||||
return;
|
||||
}
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
if ($entity->isDefaultRevision()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$component = $display->getComponent('content_moderation_control');
|
||||
if ($component) {
|
||||
$build['content_moderation_control'] = $this->formBuilder->getForm(EntityModerationForm::class, $entity);
|
||||
$build['content_moderation_control']['#weight'] = $component['weight'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the default revision for the given entity is published.
|
||||
*
|
||||
* The default revision is the same as the entity retrieved by "default" from
|
||||
* the storage handler. If the entity is translated, use the default revision
|
||||
* of the same language as the given entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being saved.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the default revision is published. FALSE otherwise.
|
||||
*/
|
||||
protected function isDefaultRevisionPublished(EntityInterface $entity) {
|
||||
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
|
||||
$default_revision = $storage->load($entity->id());
|
||||
|
||||
// Ensure we are comparing the same translation as the current entity.
|
||||
if ($default_revision instanceof TranslatableInterface && $default_revision->isTranslatable()) {
|
||||
// If there is no translation, then there is no default revision and is
|
||||
// therefore not published.
|
||||
if (!$default_revision->hasTranslation($entity->language()->getId())) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$default_revision = $default_revision->getTranslation($entity->language()->getId());
|
||||
}
|
||||
|
||||
return $default_revision && $default_revision->moderation_state->entity->isPublishedState();
|
||||
}
|
||||
|
||||
}
|
408
web/core/modules/content_moderation/src/EntityTypeInfo.php
Normal file
408
web/core/modules/content_moderation/src/EntityTypeInfo.php
Normal file
|
@ -0,0 +1,408 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList;
|
||||
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
|
||||
use Drupal\Core\Entity\BundleEntityFormBase;
|
||||
use Drupal\Core\Entity\ContentEntityFormInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\ContentEntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\content_moderation\Entity\Handler\BlockContentModerationHandler;
|
||||
use Drupal\content_moderation\Entity\Handler\ModerationHandler;
|
||||
use Drupal\content_moderation\Entity\Handler\NodeModerationHandler;
|
||||
use Drupal\content_moderation\Form\BundleModerationConfigurationForm;
|
||||
use Drupal\content_moderation\Routing\EntityModerationRouteProvider;
|
||||
use Drupal\content_moderation\Routing\EntityTypeModerationRouteProvider;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Manipulates entity type information.
|
||||
*
|
||||
* This class contains primarily bridged hooks for compile-time or
|
||||
* cache-clear-time hooks. Runtime hooks should be placed in EntityOperations.
|
||||
*/
|
||||
class EntityTypeInfo implements ContainerInjectionInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The current user.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* A keyed array of custom moderation handlers for given entity types.
|
||||
*
|
||||
* Any entity not specified will use a common default.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $moderationHandlers = [
|
||||
'node' => NodeModerationHandler::class,
|
||||
'block_content' => BlockContentModerationHandler::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* EntityTypeInfo constructor.
|
||||
*
|
||||
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation
|
||||
* The translation service. for form alters.
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
|
||||
* The moderation information service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* Entity type manager.
|
||||
*/
|
||||
public function __construct(TranslationInterface $translation, ModerationInformationInterface $moderation_information, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user) {
|
||||
$this->stringTranslation = $translation;
|
||||
$this->moderationInfo = $moderation_information;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->currentUser = $current_user;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('string_translation'),
|
||||
$container->get('content_moderation.moderation_information'),
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('current_user')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds Moderation configuration to appropriate entity types.
|
||||
*
|
||||
* This is an alter hook bridge.
|
||||
*
|
||||
* @param EntityTypeInterface[] $entity_types
|
||||
* The master entity type list to alter.
|
||||
*
|
||||
* @see hook_entity_type_alter()
|
||||
*/
|
||||
public function entityTypeAlter(array &$entity_types) {
|
||||
foreach ($this->filterNonRevisionableEntityTypes($entity_types) as $type_name => $type) {
|
||||
$entity_types[$type_name] = $this->addModerationToEntityType($type);
|
||||
$entity_types[$type->get('bundle_of')] = $this->addModerationToEntity($entity_types[$type->get('bundle_of')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies an entity definition to include moderation support.
|
||||
*
|
||||
* This primarily just means an extra handler. A Generic one is provided,
|
||||
* but individual entity types can provide their own as appropriate.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityTypeInterface $type
|
||||
* The content entity definition to modify.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\ContentEntityTypeInterface
|
||||
* The modified content entity definition.
|
||||
*/
|
||||
protected function addModerationToEntity(ContentEntityTypeInterface $type) {
|
||||
if (!$type->hasHandlerClass('moderation')) {
|
||||
$handler_class = !empty($this->moderationHandlers[$type->id()]) ? $this->moderationHandlers[$type->id()] : ModerationHandler::class;
|
||||
$type->setHandlerClass('moderation', $handler_class);
|
||||
}
|
||||
|
||||
if (!$type->hasLinkTemplate('latest-version') && $type->hasLinkTemplate('canonical')) {
|
||||
$type->setLinkTemplate('latest-version', $type->getLinkTemplate('canonical') . '/latest');
|
||||
}
|
||||
|
||||
// @todo Core forgot to add a direct way to manipulate route_provider, so
|
||||
// we have to do it the sloppy way for now.
|
||||
$providers = $type->getRouteProviderClasses() ?: [];
|
||||
if (empty($providers['moderation'])) {
|
||||
$providers['moderation'] = EntityModerationRouteProvider::class;
|
||||
$type->setHandlerClass('route_provider', $providers);
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures moderation configuration support on a entity type definition.
|
||||
*
|
||||
* That "configuration support" includes a configuration form, a hypermedia
|
||||
* link, and a route provider to tie it all together. There's also a
|
||||
* moderation handler for per-entity-type variation.
|
||||
*
|
||||
* @param \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $type
|
||||
* The config entity definition to modify.
|
||||
*
|
||||
* @return \Drupal\Core\Config\Entity\ConfigEntityTypeInterface
|
||||
* The modified config entity definition.
|
||||
*/
|
||||
protected function addModerationToEntityType(ConfigEntityTypeInterface $type) {
|
||||
if ($type->hasLinkTemplate('edit-form') && !$type->hasLinkTemplate('moderation-form')) {
|
||||
$type->setLinkTemplate('moderation-form', $type->getLinkTemplate('edit-form') . '/moderation');
|
||||
}
|
||||
|
||||
if (!$type->getFormClass('moderation')) {
|
||||
$type->setFormClass('moderation', BundleModerationConfigurationForm::class);
|
||||
}
|
||||
|
||||
// @todo Core forgot to add a direct way to manipulate route_provider, so
|
||||
// we have to do it the sloppy way for now.
|
||||
$providers = $type->getRouteProviderClasses() ?: [];
|
||||
if (empty($providers['moderation'])) {
|
||||
$providers['moderation'] = EntityTypeModerationRouteProvider::class;
|
||||
$type->setHandlerClass('route_provider', $providers);
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an operation on bundles that should have a Moderation form.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity on which to define an operation.
|
||||
*
|
||||
* @return array
|
||||
* An array of operation definitions.
|
||||
*
|
||||
* @see hook_entity_operation()
|
||||
*/
|
||||
public function entityOperation(EntityInterface $entity) {
|
||||
$operations = [];
|
||||
$type = $entity->getEntityType();
|
||||
$bundle_of = $type->getBundleOf();
|
||||
if ($this->currentUser->hasPermission('administer moderation states') && $bundle_of &&
|
||||
$this->moderationInfo->canModerateEntitiesOfEntityType($this->entityTypeManager->getDefinition($bundle_of))
|
||||
) {
|
||||
$operations['manage-moderation'] = [
|
||||
'title' => t('Manage moderation'),
|
||||
'weight' => 27,
|
||||
'url' => Url::fromRoute("entity.{$type->id()}.moderation", [$entity->getEntityTypeId() => $entity->id()]),
|
||||
];
|
||||
}
|
||||
|
||||
return $operations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the "extra fields" for a bundle.
|
||||
*
|
||||
* This is a hook bridge.
|
||||
*
|
||||
* @see hook_entity_extra_field_info()
|
||||
*
|
||||
* @return array
|
||||
* A nested array of 'pseudo-field' elements. Each list is nested within the
|
||||
* following keys: entity type, bundle name, context (either 'form' or
|
||||
* 'display'). The keys are the name of the elements as appearing in the
|
||||
* renderable array (either the entity form or the displayed entity). The
|
||||
* value is an associative array:
|
||||
* - label: The human readable name of the element. Make sure you sanitize
|
||||
* this appropriately.
|
||||
* - description: A short description of the element contents.
|
||||
* - weight: The default weight of the element.
|
||||
* - visible: (optional) The default visibility of the element. Defaults to
|
||||
* TRUE.
|
||||
* - edit: (optional) String containing markup (normally a link) used as the
|
||||
* element's 'edit' operation in the administration interface. Only for
|
||||
* 'form' context.
|
||||
* - delete: (optional) String containing markup (normally a link) used as
|
||||
* the element's 'delete' operation in the administration interface. Only
|
||||
* for 'form' context.
|
||||
*/
|
||||
public function entityExtraFieldInfo() {
|
||||
$return = [];
|
||||
foreach ($this->getModeratedBundles() as $bundle) {
|
||||
$return[$bundle['entity']][$bundle['bundle']]['display']['content_moderation_control'] = [
|
||||
'label' => $this->t('Moderation control'),
|
||||
'description' => $this->t("Status listing and form for the entity's moderation state."),
|
||||
'weight' => -20,
|
||||
'visible' => TRUE,
|
||||
];
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterable list of entity names and bundle names under moderation.
|
||||
*
|
||||
* That is, this method returns a list of bundles that have Content
|
||||
* Moderation enabled on them.
|
||||
*
|
||||
* @return \Generator
|
||||
* A generator, yielding a 2 element associative array:
|
||||
* - entity: The machine name of an entity type, such as "node" or
|
||||
* "block_content".
|
||||
* - bundle: The machine name of a bundle, such as "page" or "article".
|
||||
*/
|
||||
protected function getModeratedBundles() {
|
||||
/** @var ConfigEntityTypeInterface $type */
|
||||
foreach ($this->filterNonRevisionableEntityTypes($this->entityTypeManager->getDefinitions()) as $type_name => $type) {
|
||||
$result = $this->entityTypeManager
|
||||
->getStorage($type_name)
|
||||
->getQuery()
|
||||
->condition('third_party_settings.content_moderation.enabled', TRUE)
|
||||
->execute();
|
||||
|
||||
foreach ($result as $bundle_name) {
|
||||
yield ['entity' => $type->getBundleOf(), 'bundle' => $bundle_name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds base field info to an entity type.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* Entity type for adding base fields to.
|
||||
*
|
||||
* @return \Drupal\Core\Field\BaseFieldDefinition[]
|
||||
* New fields added by moderation state.
|
||||
*/
|
||||
public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
|
||||
if (!$this->moderationInfo->canModerateEntitiesOfEntityType($entity_type)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fields = [];
|
||||
$fields['moderation_state'] = BaseFieldDefinition::create('entity_reference')
|
||||
->setLabel($this->t('Moderation state'))
|
||||
->setDescription($this->t('The moderation state of this piece of content.'))
|
||||
->setComputed(TRUE)
|
||||
->setClass(ModerationStateFieldItemList::class)
|
||||
->setSetting('target_type', 'moderation_state')
|
||||
->setDisplayOptions('view', [
|
||||
'label' => 'hidden',
|
||||
'type' => 'hidden',
|
||||
'weight' => -5,
|
||||
])
|
||||
->setDisplayOptions('form', [
|
||||
'type' => 'moderation_state_default',
|
||||
'weight' => 5,
|
||||
'settings' => [],
|
||||
])
|
||||
->addConstraint('ModerationState', [])
|
||||
->setDisplayConfigurable('form', FALSE)
|
||||
->setDisplayConfigurable('view', FALSE)
|
||||
->setReadOnly(FALSE)
|
||||
->setTranslatable(TRUE);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds ModerationState constraint to bundles whose entities are moderated.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldDefinitionInterface[] $fields
|
||||
* The array of bundle field definitions.
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type definition.
|
||||
* @param string $bundle
|
||||
* The bundle.
|
||||
*
|
||||
* @see hook_entity_bundle_field_info_alter();
|
||||
*/
|
||||
public function entityBundleFieldInfoAlter(&$fields, EntityTypeInterface $entity_type, $bundle) {
|
||||
if (!empty($fields['moderation_state']) && $this->moderationInfo->shouldModerateEntitiesOfBundle($entity_type, $bundle)) {
|
||||
$fields['moderation_state']->addConstraint('ModerationState', []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters bundle forms to enforce revision handling.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param string $form_id
|
||||
* The form id.
|
||||
*
|
||||
* @see hook_form_alter()
|
||||
*/
|
||||
public function formAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
$form_object = $form_state->getFormObject();
|
||||
if ($form_object instanceof BundleEntityFormBase) {
|
||||
$type = $form_object->getEntity()->getEntityType();
|
||||
if ($this->moderationInfo->canModerateEntitiesOfEntityType($type)) {
|
||||
$this->entityTypeManager->getHandler($type->getBundleOf(), 'moderation')->enforceRevisionsBundleFormAlter($form, $form_state, $form_id);
|
||||
}
|
||||
}
|
||||
elseif ($form_object instanceof ContentEntityFormInterface) {
|
||||
$entity = $form_object->getEntity();
|
||||
if ($this->moderationInfo->isModeratedEntity($entity)) {
|
||||
$this->entityTypeManager
|
||||
->getHandler($entity->getEntityTypeId(), 'moderation')
|
||||
->enforceRevisionsEntityFormAlter($form, $form_state, $form_id);
|
||||
// Submit handler to redirect to the latest version, if available.
|
||||
$form['actions']['submit']['#submit'][] = [EntityTypeInfo::class, 'bundleFormRedirect'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect content entity edit forms on save, if there is a forward revision.
|
||||
*
|
||||
* When saving their changes, editors should see those changes displayed on
|
||||
* the next page.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*/
|
||||
public static function bundleFormRedirect(array &$form, FormStateInterface $form_state) {
|
||||
/* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $form_state->getFormObject()->getEntity();
|
||||
|
||||
$moderation_info = \Drupal::getContainer()->get('content_moderation.moderation_information');
|
||||
if ($moderation_info->hasForwardRevision($entity) && $entity->hasLinkTemplate('latest-version')) {
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
$form_state->setRedirect("entity.$entity_type_id.latest_version", [$entity_type_id => $entity->id()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters entity type lists to return only revisionable entity types.
|
||||
*
|
||||
* @param EntityTypeInterface[] $entity_types
|
||||
* The master entity type list filter.
|
||||
*
|
||||
* @return \Drupal\Core\Config\Entity\ConfigEntityTypeInterface[]
|
||||
* An array of revisionable entity types which are configuration entities.
|
||||
*/
|
||||
protected function filterNonRevisionableEntityTypes(array $entity_types) {
|
||||
return array_filter($entity_types, function (EntityTypeInterface $type) use ($entity_types) {
|
||||
return ($type instanceof ConfigEntityTypeInterface)
|
||||
&& ($bundle_of = $type->get('bundle_of'))
|
||||
&& $entity_types[$bundle_of]->isRevisionable();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Form;
|
||||
|
||||
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
|
||||
use Drupal\Core\Entity\EntityForm;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\content_moderation\Entity\ModerationState;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Form for configuring moderation usage on a given entity bundle.
|
||||
*/
|
||||
class BundleModerationConfigurationForm extends EntityForm {
|
||||
|
||||
/**
|
||||
* Entity Type Manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static($container->get('entity_type.manager'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Blank out the base form ID so that form alters that use the base form ID to
|
||||
* target both add and edit forms don't pick up this form.
|
||||
*/
|
||||
public function getBaseFormId() {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function form(array $form, FormStateInterface $form_state) {
|
||||
/* @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $bundle */
|
||||
$bundle = $form_state->getFormObject()->getEntity();
|
||||
$form['enable_moderation_state'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Enable moderation states.'),
|
||||
'#description' => $this->t('Content of this type must transition through moderation states in order to be published.'),
|
||||
'#default_value' => $bundle->getThirdPartySetting('content_moderation', 'enabled', FALSE),
|
||||
];
|
||||
|
||||
// Add a special message when moderation is being disabled.
|
||||
if ($bundle->getThirdPartySetting('content_moderation', 'enabled', FALSE)) {
|
||||
$form['enable_moderation_state_note'] = [
|
||||
'#type' => 'item',
|
||||
'#description' => $this->t('After disabling moderation, any existing forward drafts will be accessible via the "Revisions" tab.'),
|
||||
'#states' => [
|
||||
'visible' => [
|
||||
':input[name=enable_moderation_state]' => ['checked' => FALSE],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$states = $this->entityTypeManager->getStorage('moderation_state')->loadMultiple();
|
||||
$label = function(ModerationState $state) {
|
||||
return $state->label();
|
||||
};
|
||||
|
||||
$options_published = array_map($label, array_filter($states, function(ModerationState $state) {
|
||||
return $state->isPublishedState();
|
||||
}));
|
||||
|
||||
$options_unpublished = array_map($label, array_filter($states, function(ModerationState $state) {
|
||||
return !$state->isPublishedState();
|
||||
}));
|
||||
|
||||
$form['allowed_moderation_states_unpublished'] = [
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => $this->t('Allowed moderation states (Unpublished)'),
|
||||
'#description' => $this->t('The allowed unpublished moderation states this content-type can be assigned.'),
|
||||
'#default_value' => $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys($options_unpublished)),
|
||||
'#options' => $options_unpublished,
|
||||
'#required' => TRUE,
|
||||
'#states' => [
|
||||
'visible' => [
|
||||
':input[name=enable_moderation_state]' => ['checked' => TRUE],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$form['allowed_moderation_states_published'] = [
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => $this->t('Allowed moderation states (Published)'),
|
||||
'#description' => $this->t('The allowed published moderation states this content-type can be assigned.'),
|
||||
'#default_value' => $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys($options_published)),
|
||||
'#options' => $options_published,
|
||||
'#required' => TRUE,
|
||||
'#states' => [
|
||||
'visible' => [
|
||||
':input[name=enable_moderation_state]' => ['checked' => TRUE],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// The key of the array needs to be a user-facing string so we have to fully
|
||||
// render the translatable string to a real string, or else PHP errors on an
|
||||
// object used as an array key.
|
||||
$options = [
|
||||
$this->t('Unpublished')->render() => $options_unpublished,
|
||||
$this->t('Published')->render() => $options_published,
|
||||
];
|
||||
|
||||
$form['default_moderation_state'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Default moderation state'),
|
||||
'#options' => $options,
|
||||
'#description' => $this->t('Select the moderation state for new content'),
|
||||
'#default_value' => $bundle->getThirdPartySetting('content_moderation', 'default_moderation_state', 'draft'),
|
||||
'#states' => [
|
||||
'visible' => [
|
||||
':input[name=enable_moderation_state]' => ['checked' => TRUE],
|
||||
],
|
||||
],
|
||||
];
|
||||
$form['#entity_builders'][] = [$this, 'formBuilderCallback'];
|
||||
|
||||
return parent::form($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form builder callback.
|
||||
*
|
||||
* @todo This should be folded into the form method.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type identifier.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $bundle
|
||||
* The bundle entity updated with the submitted values.
|
||||
* @param array $form
|
||||
* The complete form array.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*/
|
||||
public function formBuilderCallback($entity_type_id, EntityInterface $bundle, &$form, FormStateInterface $form_state) {
|
||||
// @todo https://www.drupal.org/node/2779933 write a test for this.
|
||||
if ($bundle instanceof ThirdPartySettingsInterface) {
|
||||
$bundle->setThirdPartySetting('content_moderation', 'enabled', $form_state->getValue('enable_moderation_state'));
|
||||
$bundle->setThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys(array_filter($form_state->getValue('allowed_moderation_states_published') + $form_state->getValue('allowed_moderation_states_unpublished'))));
|
||||
$bundle->setThirdPartySetting('content_moderation', 'default_moderation_state', $form_state->getValue('default_moderation_state'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateForm(array &$form, FormStateInterface $form_state) {
|
||||
if ($form_state->getValue('enable_moderation_state')) {
|
||||
$allowed = array_keys(array_filter($form_state->getValue('allowed_moderation_states_published') + $form_state->getValue('allowed_moderation_states_unpublished')));
|
||||
|
||||
if (($default = $form_state->getValue('default_moderation_state')) && !in_array($default, $allowed, TRUE)) {
|
||||
$form_state->setErrorByName('default_moderation_state', $this->t('The default moderation state must be one of the allowed states.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
// If moderation is enabled, revisions MUST be enabled as well. Otherwise we
|
||||
// can't have forward revisions.
|
||||
if ($form_state->getValue('enable_moderation_state')) {
|
||||
/* @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $bundle */
|
||||
$bundle = $form_state->getFormObject()->getEntity();
|
||||
|
||||
$this->entityTypeManager->getHandler($bundle->getEntityType()->getBundleOf(), 'moderation')->onBundleModerationConfigurationFormSubmit($bundle);
|
||||
}
|
||||
|
||||
parent::submitForm($form, $form_state);
|
||||
|
||||
drupal_set_message($this->t('Your settings have been saved.'));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Form;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\content_moderation\Entity\ModerationStateTransition;
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Drupal\content_moderation\StateTransitionValidation;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* The EntityModerationForm provides a simple UI for changing moderation state.
|
||||
*/
|
||||
class EntityModerationForm extends FormBase {
|
||||
|
||||
/**
|
||||
* The moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* The moderation state transition validation service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\StateTransitionValidation
|
||||
*/
|
||||
protected $validation;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* EntityModerationForm constructor.
|
||||
*
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
|
||||
* The moderation information service.
|
||||
* @param \Drupal\content_moderation\StateTransitionValidation $validation
|
||||
* The moderation state transition validation service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
*/
|
||||
public function __construct(ModerationInformationInterface $moderation_info, StateTransitionValidation $validation, EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->moderationInfo = $moderation_info;
|
||||
$this->validation = $validation;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('content_moderation.moderation_information'),
|
||||
$container->get('content_moderation.state_transition_validation'),
|
||||
$container->get('entity_type.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'content_moderation_entity_moderation_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, ContentEntityInterface $entity = NULL) {
|
||||
/** @var \Drupal\content_moderation\Entity\ModerationState $current_state */
|
||||
$current_state = $entity->moderation_state->entity;
|
||||
|
||||
$transitions = $this->validation->getValidTransitions($entity, $this->currentUser());
|
||||
|
||||
// Exclude self-transitions.
|
||||
$transitions = array_filter($transitions, function(ModerationStateTransition $transition) use ($current_state) {
|
||||
return $transition->getToState() != $current_state->id();
|
||||
});
|
||||
|
||||
$target_states = [];
|
||||
/** @var ModerationStateTransition $transition */
|
||||
foreach ($transitions as $transition) {
|
||||
$target_states[$transition->getToState()] = $transition->label();
|
||||
}
|
||||
|
||||
if (!count($target_states)) {
|
||||
return $form;
|
||||
}
|
||||
|
||||
if ($current_state) {
|
||||
$form['current'] = [
|
||||
'#type' => 'item',
|
||||
'#title' => $this->t('Status'),
|
||||
'#markup' => $current_state->label(),
|
||||
];
|
||||
}
|
||||
|
||||
// Persist the entity so we can access it in the submit handler.
|
||||
$form_state->set('entity', $entity);
|
||||
|
||||
$form['new_state'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Moderate'),
|
||||
'#options' => $target_states,
|
||||
];
|
||||
|
||||
$form['revision_log'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Log message'),
|
||||
'#size' => 30,
|
||||
];
|
||||
|
||||
$form['submit'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Apply'),
|
||||
];
|
||||
|
||||
$form['#theme'] = ['entity_moderation_form'];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
/** @var ContentEntityInterface $entity */
|
||||
$entity = $form_state->get('entity');
|
||||
|
||||
$new_state = $form_state->getValue('new_state');
|
||||
|
||||
// @todo should we just just be updating the content moderation state
|
||||
// entity? That would prevent setting the revision log.
|
||||
$entity->moderation_state->target_id = $new_state;
|
||||
$entity->revision_log = $form_state->getValue('revision_log');
|
||||
|
||||
$entity->save();
|
||||
|
||||
drupal_set_message($this->t('The moderation state has been updated.'));
|
||||
|
||||
/** @var \Drupal\content_moderation\Entity\ModerationState $state */
|
||||
$state = $this->entityTypeManager->getStorage('moderation_state')->load($new_state);
|
||||
|
||||
// The page we're on likely won't be visible if we just set the entity to
|
||||
// the default state, as we hide that latest-revision tab if there is no
|
||||
// forward revision. Redirect to the canonical URL instead, since that will
|
||||
// still exist.
|
||||
if ($state->isDefaultRevisionState()) {
|
||||
$form_state->setRedirectUrl($entity->toUrl('canonical'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Form;
|
||||
|
||||
use Drupal\Core\Entity\EntityConfirmFormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Builds the form to delete Moderation state entities.
|
||||
*/
|
||||
class ModerationStateDeleteForm extends EntityConfirmFormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getQuestion() {
|
||||
return $this->t('Are you sure you want to delete %name?', array('%name' => $this->entity->label()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCancelUrl() {
|
||||
return new Url('entity.moderation_state.collection');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfirmText() {
|
||||
return $this->t('Delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$this->entity->delete();
|
||||
|
||||
drupal_set_message($this->t(
|
||||
'Moderation state %label deleted.',
|
||||
['%label' => $this->entity->label()]
|
||||
));
|
||||
|
||||
$form_state->setRedirectUrl($this->getCancelUrl());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Form;
|
||||
|
||||
use Drupal\content_moderation\Entity\ModerationState;
|
||||
use Drupal\Core\Entity\EntityForm;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Class ModerationStateForm.
|
||||
*/
|
||||
class ModerationStateForm extends EntityForm {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function form(array $form, FormStateInterface $form_state) {
|
||||
$form = parent::form($form, $form_state);
|
||||
|
||||
/* @var \Drupal\content_moderation\ModerationStateInterface $moderation_state */
|
||||
$moderation_state = $this->entity;
|
||||
$form['label'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Label'),
|
||||
'#maxlength' => 255,
|
||||
'#default_value' => $moderation_state->label(),
|
||||
'#description' => $this->t('Label for the Moderation state.'),
|
||||
'#required' => TRUE,
|
||||
);
|
||||
|
||||
$form['id'] = array(
|
||||
'#type' => 'machine_name',
|
||||
'#default_value' => $moderation_state->id(),
|
||||
'#machine_name' => array(
|
||||
'exists' => [ModerationState::class, 'load'],
|
||||
),
|
||||
'#disabled' => !$moderation_state->isNew(),
|
||||
);
|
||||
|
||||
$form['published'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Published'),
|
||||
'#description' => $this->t('When content reaches this state it should be published.'),
|
||||
'#default_value' => $moderation_state->isPublishedState(),
|
||||
];
|
||||
|
||||
$form['default_revision'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Default revision'),
|
||||
'#description' => $this->t('When content reaches this state it should be made the default revision; this is implied for published states.'),
|
||||
'#default_value' => $moderation_state->isDefaultRevisionState(),
|
||||
// @todo Add form #state to force "make default" on when "published" is
|
||||
// on for a state.
|
||||
// @see https://www.drupal.org/node/2645614
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(array $form, FormStateInterface $form_state) {
|
||||
$moderation_state = $this->entity;
|
||||
$status = $moderation_state->save();
|
||||
|
||||
switch ($status) {
|
||||
case SAVED_NEW:
|
||||
drupal_set_message($this->t('Created the %label Moderation state.', [
|
||||
'%label' => $moderation_state->label(),
|
||||
]));
|
||||
break;
|
||||
|
||||
default:
|
||||
drupal_set_message($this->t('Saved the %label Moderation state.', [
|
||||
'%label' => $moderation_state->label(),
|
||||
]));
|
||||
}
|
||||
$form_state->setRedirectUrl($moderation_state->toUrl('collection'));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Form;
|
||||
|
||||
use Drupal\Core\Entity\EntityConfirmFormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Builds the form to delete Moderation state transition entities.
|
||||
*/
|
||||
class ModerationStateTransitionDeleteForm extends EntityConfirmFormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getQuestion() {
|
||||
return $this->t('Are you sure you want to delete %name?', array('%name' => $this->entity->label()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCancelUrl() {
|
||||
return new Url('entity.moderation_state_transition.collection');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfirmText() {
|
||||
return $this->t('Delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$this->entity->delete();
|
||||
|
||||
drupal_set_message($this->t(
|
||||
'Moderation transition %label deleted.',
|
||||
['%label' => $this->entity->label()]
|
||||
));
|
||||
|
||||
$form_state->setRedirectUrl($this->getCancelUrl());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Form;
|
||||
|
||||
use Drupal\Core\Entity\EntityForm;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\Query\QueryFactory;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Class ModerationStateTransitionForm.
|
||||
*
|
||||
* @package Drupal\content_moderation\Form
|
||||
*/
|
||||
class ModerationStateTransitionForm extends EntityForm {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity query factory.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\Query\QueryFactory
|
||||
*/
|
||||
protected $queryFactory;
|
||||
|
||||
/**
|
||||
* Constructs a new ModerationStateTransitionForm.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
|
||||
* The entity query factory.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, QueryFactory $query_factory) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->queryFactory = $query_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static($container->get('entity_type.manager'), $container->get('entity.query'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function form(array $form, FormStateInterface $form_state) {
|
||||
$form = parent::form($form, $form_state);
|
||||
|
||||
/* @var \Drupal\content_moderation\ModerationStateTransitionInterface $moderation_state_transition */
|
||||
$moderation_state_transition = $this->entity;
|
||||
$form['label'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Label'),
|
||||
'#maxlength' => 255,
|
||||
'#default_value' => $moderation_state_transition->label(),
|
||||
'#description' => $this->t('Label for the Moderation state transition.'),
|
||||
'#required' => TRUE,
|
||||
];
|
||||
|
||||
$form['id'] = [
|
||||
'#type' => 'machine_name',
|
||||
'#default_value' => $moderation_state_transition->id(),
|
||||
'#machine_name' => [
|
||||
'exists' => '\Drupal\content_moderation\Entity\ModerationStateTransition::load',
|
||||
],
|
||||
'#disabled' => !$moderation_state_transition->isNew(),
|
||||
];
|
||||
|
||||
$options = [];
|
||||
foreach ($this->entityTypeManager->getStorage('moderation_state')
|
||||
->loadMultiple() as $moderation_state) {
|
||||
$options[$moderation_state->id()] = $moderation_state->label();
|
||||
}
|
||||
|
||||
$form['container'] = [
|
||||
'#type' => 'container',
|
||||
'#attributes' => [
|
||||
'class' => ['container-inline'],
|
||||
],
|
||||
];
|
||||
|
||||
$form['container']['stateFrom'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Transition from'),
|
||||
'#options' => $options,
|
||||
'#required' => TRUE,
|
||||
'#empty_option' => $this->t('-- Select --'),
|
||||
'#default_value' => $moderation_state_transition->getFromState(),
|
||||
];
|
||||
|
||||
$form['container']['stateTo'] = [
|
||||
'#type' => 'select',
|
||||
'#options' => $options,
|
||||
'#required' => TRUE,
|
||||
'#title' => $this->t('Transition to'),
|
||||
'#empty_option' => $this->t('-- Select --'),
|
||||
'#default_value' => $moderation_state_transition->getToState(),
|
||||
];
|
||||
|
||||
// Make sure there's always at least a wide enough delta on weight to cover
|
||||
// the current value or the total number of transitions. That way we
|
||||
// never end up forcing a transition to change its weight needlessly.
|
||||
$num_transitions = $this->queryFactory->get('moderation_state_transition')
|
||||
->count()
|
||||
->execute();
|
||||
$delta = max(abs($moderation_state_transition->getWeight()), $num_transitions);
|
||||
|
||||
$form['weight'] = [
|
||||
'#type' => 'weight',
|
||||
'#delta' => $delta,
|
||||
'#options' => $options,
|
||||
'#title' => $this->t('Weight'),
|
||||
'#default_value' => $moderation_state_transition->getWeight(),
|
||||
'#description' => $this->t('Orders the transitions in moderation forms and the administrative listing. Heavier items will sink and the lighter items will be positioned nearer the top.'),
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(array $form, FormStateInterface $form_state) {
|
||||
$moderation_state_transition = $this->entity;
|
||||
$status = $moderation_state_transition->save();
|
||||
|
||||
switch ($status) {
|
||||
case SAVED_NEW:
|
||||
drupal_set_message($this->t('Created the %label Moderation state transition.', [
|
||||
'%label' => $moderation_state_transition->label(),
|
||||
]));
|
||||
break;
|
||||
|
||||
default:
|
||||
drupal_set_message($this->t('Saved the %label Moderation state transition.', [
|
||||
'%label' => $moderation_state_transition->label(),
|
||||
]));
|
||||
}
|
||||
$form_state->setRedirectUrl($moderation_state_transition->toUrl('collection'));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
|
||||
/**
|
||||
* General service for moderation-related questions about Entity API.
|
||||
*/
|
||||
class ModerationInformation implements ModerationInformationInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Creates a new ModerationInformation instance.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Session\AccountInterface $current_user
|
||||
* The current user.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isModeratedEntity(EntityInterface $entity) {
|
||||
if (!$entity instanceof ContentEntityInterface) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return $this->shouldModerateEntitiesOfBundle($entity->getEntityType(), $entity->bundle());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function canModerateEntitiesOfEntityType(EntityTypeInterface $entity_type) {
|
||||
return $entity_type->hasHandlerClass('moderation');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function shouldModerateEntitiesOfBundle(EntityTypeInterface $entity_type, $bundle) {
|
||||
if ($this->canModerateEntitiesOfEntityType($entity_type)) {
|
||||
$bundle_entity = $this->entityTypeManager->getStorage($entity_type->getBundleEntityType())->load($bundle);
|
||||
if ($bundle_entity) {
|
||||
return $bundle_entity->getThirdPartySetting('content_moderation', 'enabled', FALSE);
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLatestRevision($entity_type_id, $entity_id) {
|
||||
if ($latest_revision_id = $this->getLatestRevisionId($entity_type_id, $entity_id)) {
|
||||
return $this->entityTypeManager->getStorage($entity_type_id)->loadRevision($latest_revision_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLatestRevisionId($entity_type_id, $entity_id) {
|
||||
if ($storage = $this->entityTypeManager->getStorage($entity_type_id)) {
|
||||
$revision_ids = $storage->getQuery()
|
||||
->allRevisions()
|
||||
->condition($this->entityTypeManager->getDefinition($entity_type_id)->getKey('id'), $entity_id)
|
||||
->sort($this->entityTypeManager->getDefinition($entity_type_id)->getKey('revision'), 'DESC')
|
||||
->range(0, 1)
|
||||
->execute();
|
||||
if ($revision_ids) {
|
||||
return array_keys($revision_ids)[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefaultRevisionId($entity_type_id, $entity_id) {
|
||||
if ($storage = $this->entityTypeManager->getStorage($entity_type_id)) {
|
||||
$revision_ids = $storage->getQuery()
|
||||
->condition($this->entityTypeManager->getDefinition($entity_type_id)->getKey('id'), $entity_id)
|
||||
->sort($this->entityTypeManager->getDefinition($entity_type_id)->getKey('revision'), 'DESC')
|
||||
->range(0, 1)
|
||||
->execute();
|
||||
if ($revision_ids) {
|
||||
return array_keys($revision_ids)[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isLatestRevision(ContentEntityInterface $entity) {
|
||||
return $entity->getRevisionId() == $this->getLatestRevisionId($entity->getEntityTypeId(), $entity->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function hasForwardRevision(ContentEntityInterface $entity) {
|
||||
return $this->isModeratedEntity($entity)
|
||||
&& !($this->getLatestRevisionId($entity->getEntityTypeId(), $entity->id()) == $this->getDefaultRevisionId($entity->getEntityTypeId(), $entity->id()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isLiveRevision(ContentEntityInterface $entity) {
|
||||
return $this->isLatestRevision($entity)
|
||||
&& $entity->isDefaultRevision()
|
||||
&& $entity->moderation_state->entity
|
||||
&& $entity->moderation_state->entity->isPublishedState();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
|
||||
/**
|
||||
* Interface for moderation_information service.
|
||||
*/
|
||||
interface ModerationInformationInterface {
|
||||
|
||||
/**
|
||||
* Determines if an entity is moderated.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity we may be moderating.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if this entity is moderated, FALSE otherwise.
|
||||
*/
|
||||
public function isModeratedEntity(EntityInterface $entity);
|
||||
|
||||
/**
|
||||
* Determines if an entity type can have moderated entities.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* An entity type object.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if this entity type can have moderated entities, FALSE otherwise.
|
||||
*/
|
||||
public function canModerateEntitiesOfEntityType(EntityTypeInterface $entity_type);
|
||||
|
||||
/**
|
||||
* Determines if an entity type/bundle entities should be moderated.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type definition to check.
|
||||
* @param string $bundle
|
||||
* The bundle to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if an entity type/bundle entities should be moderated, FALSE
|
||||
* otherwise.
|
||||
*/
|
||||
public function shouldModerateEntitiesOfBundle(EntityTypeInterface $entity_type, $bundle);
|
||||
|
||||
/**
|
||||
* Loads the latest revision of a specific entity.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param int $entity_id
|
||||
* The entity ID.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\ContentEntityInterface|null
|
||||
* The latest entity revision or NULL, if the entity type / entity doesn't
|
||||
* exist.
|
||||
*/
|
||||
public function getLatestRevision($entity_type_id, $entity_id);
|
||||
|
||||
/**
|
||||
* Returns the revision ID of the latest revision of the given entity.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param int $entity_id
|
||||
* The entity ID.
|
||||
*
|
||||
* @return int
|
||||
* The revision ID of the latest revision for the specified entity, or
|
||||
* NULL if there is no such entity.
|
||||
*/
|
||||
public function getLatestRevisionId($entity_type_id, $entity_id);
|
||||
|
||||
/**
|
||||
* Returns the revision ID of the default revision for the specified entity.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param int $entity_id
|
||||
* The entity ID.
|
||||
*
|
||||
* @return int
|
||||
* The revision ID of the default revision, or NULL if the entity was
|
||||
* not found.
|
||||
*/
|
||||
public function getDefaultRevisionId($entity_type_id, $entity_id);
|
||||
|
||||
/**
|
||||
* Determines if an entity is a latest revision.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* A revisionable content entity.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the specified object is the latest revision of its entity,
|
||||
* FALSE otherwise.
|
||||
*/
|
||||
public function isLatestRevision(ContentEntityInterface $entity);
|
||||
|
||||
/**
|
||||
* Determines if a forward revision exists for the specified entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity which may or may not have a forward revision.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if this entity has forward revisions available, FALSE otherwise.
|
||||
*/
|
||||
public function hasForwardRevision(ContentEntityInterface $entity);
|
||||
|
||||
/**
|
||||
* Determines if an entity is "live".
|
||||
*
|
||||
* A "live" entity revision is one whose latest revision is also the default,
|
||||
* and whose moderation state, if any, is a published state.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the specified entity is a live revision, FALSE otherwise.
|
||||
*/
|
||||
public function isLiveRevision(ContentEntityInterface $entity);
|
||||
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\EntityAccessControlHandler;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
|
||||
/**
|
||||
* Access controller for the Moderation State entity.
|
||||
*
|
||||
* @see \Drupal\workbench_moderation\Entity\ModerationState.
|
||||
*/
|
||||
class ModerationStateAccessControlHandler extends EntityAccessControlHandler {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
|
||||
$admin_access = parent::checkAccess($entity, $operation, $account);
|
||||
|
||||
// Allow view with other permission.
|
||||
if ($operation === 'view') {
|
||||
return AccessResult::allowedIfHasPermission($account, 'view moderation states')->orIf($admin_access);
|
||||
}
|
||||
|
||||
return $admin_access;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface for defining Moderation state entities.
|
||||
*/
|
||||
interface ModerationStateInterface extends ConfigEntityInterface {
|
||||
|
||||
/**
|
||||
* Determines if content updated to this state should be published.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if content updated to this state should be published.
|
||||
*/
|
||||
public function isPublishedState();
|
||||
|
||||
/**
|
||||
* Determines if content updated to this state should be the default revision.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if content in this state should be the default revision.
|
||||
*/
|
||||
public function isDefaultRevisionState();
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Config\Entity\DraggableListBuilder;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
/**
|
||||
* Provides a listing of Moderation state entities.
|
||||
*/
|
||||
class ModerationStateListBuilder extends DraggableListBuilder {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'moderation_state_admin_overview_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildHeader() {
|
||||
$header['label'] = $this->t('Moderation state');
|
||||
$header['id'] = $this->t('Machine name');
|
||||
|
||||
return $header + parent::buildHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildRow(EntityInterface $entity) {
|
||||
$row['label'] = $entity->label();
|
||||
$row['id']['#markup'] = $entity->id();
|
||||
|
||||
return $row + parent::buildRow($entity);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface for defining Moderation state transition entities.
|
||||
*/
|
||||
interface ModerationStateTransitionInterface extends ConfigEntityInterface {
|
||||
|
||||
/**
|
||||
* Gets the from state for the given transition.
|
||||
*
|
||||
* @return string
|
||||
* The moderation state ID for the from state.
|
||||
*/
|
||||
public function getFromState();
|
||||
|
||||
/**
|
||||
* Gets the to state for the given transition.
|
||||
*
|
||||
* @return string
|
||||
* The moderation state ID for the to state.
|
||||
*/
|
||||
public function getToState();
|
||||
|
||||
/**
|
||||
* Gets the weight for the given transition.
|
||||
*
|
||||
* @return int
|
||||
* The weight of this transition.
|
||||
*/
|
||||
public function getWeight();
|
||||
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Config\Entity\DraggableListBuilder;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\user\RoleStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a listing of Moderation state transition entities.
|
||||
*/
|
||||
class ModerationStateTransitionListBuilder extends DraggableListBuilder {
|
||||
|
||||
/**
|
||||
* Moderation state entity storage.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityStorageInterface
|
||||
*/
|
||||
protected $stateStorage;
|
||||
|
||||
/**
|
||||
* The role storage.
|
||||
*
|
||||
* @var \Drupal\user\RoleStorageInterface
|
||||
*/
|
||||
protected $roleStorage;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
return new static(
|
||||
$entity_type,
|
||||
$container->get('entity.manager')->getStorage($entity_type->id()),
|
||||
$container->get('entity.manager')->getStorage('moderation_state'),
|
||||
$container->get('entity.manager')->getStorage('user_role')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new ModerationStateTransitionListBuilder.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* Entity Type.
|
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $transition_storage
|
||||
* Moderation state transition entity storage.
|
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $state_storage
|
||||
* Moderation state entity storage.
|
||||
* @param \Drupal\user\RoleStorageInterface $role_storage
|
||||
* The role storage.
|
||||
*/
|
||||
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $transition_storage, EntityStorageInterface $state_storage, RoleStorageInterface $role_storage) {
|
||||
parent::__construct($entity_type, $transition_storage);
|
||||
$this->stateStorage = $state_storage;
|
||||
$this->roleStorage = $role_storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'content_moderation_transition_list';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildHeader() {
|
||||
$header['to'] = $this->t('To state');
|
||||
$header['label'] = $this->t('Button label');
|
||||
$header['roles'] = $this->t('Allowed roles');
|
||||
|
||||
return $header + parent::buildHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildRow(EntityInterface $entity) {
|
||||
$row['to']['#markup'] = $this->stateStorage->load($entity->getToState())->label();
|
||||
$row['label'] = $entity->label();
|
||||
$row['roles']['#markup'] = implode(', ', user_role_names(FALSE, 'use ' . $entity->id() . ' transition'));
|
||||
|
||||
return $row + parent::buildRow($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render() {
|
||||
$build = parent::render();
|
||||
|
||||
$build['item'] = [
|
||||
'#type' => 'item',
|
||||
'#markup' => $this->t('On this screen you can define <em>transitions</em>. Every time an entity is saved, it undergoes a transition. It is not possible to save an entity if it tries do a transition not defined here. Transitions do not necessarily mean a state change, it is possible to transition from a state to the same state but that transition needs to be defined here as well.'),
|
||||
'#weight' => -5,
|
||||
];
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
$this->entities = $this->load();
|
||||
|
||||
// Get all the moderation states and sort them by weight.
|
||||
$states = $this->stateStorage->loadMultiple();
|
||||
uasort($states, array($this->entityType->getClass(), 'sort'));
|
||||
|
||||
/** @var \Drupal\content_moderation\ModerationStateTransitionInterface $entity */
|
||||
$groups = array_fill_keys(array_keys($states), []);
|
||||
foreach ($this->entities as $entity) {
|
||||
$groups[$entity->getFromState()][] = $entity;
|
||||
}
|
||||
|
||||
foreach ($groups as $group_name => $entities) {
|
||||
$form[$group_name] = [
|
||||
'#type' => 'details',
|
||||
'#title' => $this->t('From @state to...', ['@state' => $states[$group_name]->label()]),
|
||||
// Make sure that the first group is always open.
|
||||
'#open' => $group_name === array_keys($groups)[0],
|
||||
];
|
||||
|
||||
$form[$group_name][$this->entitiesKey] = array(
|
||||
'#type' => 'table',
|
||||
'#header' => $this->buildHeader(),
|
||||
'#empty' => t('There is no @label yet.', array('@label' => $this->entityType->getLabel())),
|
||||
'#tabledrag' => array(
|
||||
array(
|
||||
'action' => 'order',
|
||||
'relationship' => 'sibling',
|
||||
'group' => 'weight',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$delta = 10;
|
||||
// Change the delta of the weight field if have more than 20 entities.
|
||||
if (!empty($this->weightKey)) {
|
||||
$count = count($this->entities);
|
||||
if ($count > 20) {
|
||||
$delta = ceil($count / 2);
|
||||
}
|
||||
}
|
||||
foreach ($entities as $entity) {
|
||||
$row = $this->buildRow($entity);
|
||||
if (isset($row['label'])) {
|
||||
$row['label'] = array('#markup' => $row['label']);
|
||||
}
|
||||
if (isset($row['weight'])) {
|
||||
$row['weight']['#delta'] = $delta;
|
||||
}
|
||||
$form[$group_name][$this->entitiesKey][$entity->id()] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$form['actions']['#type'] = 'actions';
|
||||
$form['actions']['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Save order'),
|
||||
'#button_type' => 'primary',
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\ParamConverter;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
use Drupal\Core\ParamConverter\EntityConverter;
|
||||
use Drupal\Core\TypedData\TranslatableInterface;
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
* Defines a class for making sure the edit-route loads the current draft.
|
||||
*/
|
||||
class EntityRevisionConverter extends EntityConverter {
|
||||
|
||||
/**
|
||||
* Moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInformation;
|
||||
|
||||
/**
|
||||
* EntityRevisionConverter constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
|
||||
* The entity manager, needed by the parent class.
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
|
||||
* The moderation info utility service.
|
||||
*
|
||||
* @todo: If the parent class is ever cleaned up to use EntityTypeManager
|
||||
* instead of Entity manager, this method will also need to be adjusted.
|
||||
*/
|
||||
public function __construct(EntityManagerInterface $entity_manager, ModerationInformationInterface $moderation_info) {
|
||||
parent::__construct($entity_manager);
|
||||
$this->moderationInformation = $moderation_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applies($definition, $name, Route $route) {
|
||||
return $this->hasForwardRevisionFlag($definition) || $this->isEditFormPage($route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the route definition includes a forward-revision flag.
|
||||
*
|
||||
* This is a custom flag defined by the Content Moderation module to load
|
||||
* forward revisions rather than the default revision on a given route.
|
||||
*
|
||||
* @param array $definition
|
||||
* The parameter definition provided in the route options.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the forward revision flag is set, FALSE otherwise.
|
||||
*/
|
||||
protected function hasForwardRevisionFlag(array $definition) {
|
||||
return (isset($definition['load_forward_revision']) && $definition['load_forward_revision']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given route is the edit-form for an entity.
|
||||
*
|
||||
* @param \Symfony\Component\Routing\Route $route
|
||||
* The route definition.
|
||||
*
|
||||
* @return bool
|
||||
* Returns TRUE if the route is the edit form of an entity, FALSE otherwise.
|
||||
*/
|
||||
protected function isEditFormPage(Route $route) {
|
||||
if ($default = $route->getDefault('_entity_form')) {
|
||||
// If no operation is provided, use 'default'.
|
||||
$default .= '.default';
|
||||
list($entity_type_id, $operation) = explode('.', $default);
|
||||
if (!$this->entityManager->hasDefinition($entity_type_id)) {
|
||||
return FALSE;
|
||||
}
|
||||
$entity_type = $this->entityManager->getDefinition($entity_type_id);
|
||||
return $operation == 'edit' && $entity_type && $entity_type->isRevisionable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function convert($value, $definition, $name, array $defaults) {
|
||||
$entity = parent::convert($value, $definition, $name, $defaults);
|
||||
|
||||
if ($entity && $this->moderationInformation->isModeratedEntity($entity) && !$this->moderationInformation->isLatestRevision($entity)) {
|
||||
$entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults);
|
||||
$latest_revision = $this->moderationInformation->getLatestRevision($entity_type_id, $value);
|
||||
|
||||
// If the entity type is translatable, ensure we return the proper
|
||||
// translation object for the current context.
|
||||
if ($latest_revision instanceof EntityInterface && $entity instanceof TranslatableInterface) {
|
||||
$latest_revision = $this->entityManager->getTranslationFromContext($latest_revision, NULL, array('operation' => 'entity_upcast'));
|
||||
}
|
||||
|
||||
if ($latest_revision->isRevisionTranslationAffected()) {
|
||||
$entity = $latest_revision;
|
||||
}
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
}
|
43
web/core/modules/content_moderation/src/Permissions.php
Normal file
43
web/core/modules/content_moderation/src/Permissions.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\content_moderation\Entity\ModerationState;
|
||||
use Drupal\content_moderation\Entity\ModerationStateTransition;
|
||||
|
||||
/**
|
||||
* Defines a class for dynamic permissions based on transitions.
|
||||
*/
|
||||
class Permissions {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Returns an array of transition permissions.
|
||||
*
|
||||
* @return array
|
||||
* The transition permissions.
|
||||
*/
|
||||
public function transitionPermissions() {
|
||||
// @todo https://www.drupal.org/node/2779933 write a test for this.
|
||||
$perms = [];
|
||||
/* @var \Drupal\content_moderation\ModerationStateInterface[] $states */
|
||||
$states = ModerationState::loadMultiple();
|
||||
/* @var \Drupal\content_moderation\ModerationStateTransitionInterface $transition */
|
||||
foreach (ModerationStateTransition::loadMultiple() as $id => $transition) {
|
||||
$perms['use ' . $id . ' transition'] = [
|
||||
'title' => $this->t('Use the %transition_name transition', [
|
||||
'%transition_name' => $transition->label(),
|
||||
]),
|
||||
'description' => $this->t('Move content from %from state to %to state.', [
|
||||
'%from' => $states[$transition->getFromState()]->label(),
|
||||
'%to' => $states[$transition->getToState()]->label(),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
return $perms;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Action;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\node\Plugin\Action\PublishNode;
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Alternate action plugin that can opt-out of modifying moderated entities.
|
||||
*
|
||||
* @see \Drupal\node\Plugin\Action\PublishNode
|
||||
*/
|
||||
class ModerationOptOutPublishNode extends PublishNode implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* Moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* ModerationOptOutPublishNode constructor.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
|
||||
* The moderation information service.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModerationInformationInterface $moderation_info) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->moderationInfo = $moderation_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration, $plugin_id, $plugin_definition,
|
||||
$container->get('content_moderation.moderation_information')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function execute($entity = NULL) {
|
||||
if ($entity && $this->moderationInfo->isModeratedEntity($entity)) {
|
||||
drupal_set_message($this->t('One or more entities were skipped as they are under moderation and may not be directly published or unpublished.'));
|
||||
return;
|
||||
}
|
||||
|
||||
parent::execute($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
|
||||
$result = parent::access($object, $account, TRUE)
|
||||
->andif(AccessResult::forbiddenIf($this->moderationInfo->isModeratedEntity($object))->addCacheableDependency($object));
|
||||
|
||||
return $return_as_object ? $result : $result->isAllowed();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Action;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\node\Plugin\Action\UnpublishNode;
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Alternate action plugin that can opt-out of modifying moderated entities.
|
||||
*
|
||||
* @see \Drupal\node\Plugin\Action\UnpublishNode
|
||||
*/
|
||||
class ModerationOptOutUnpublishNode extends UnpublishNode implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* Moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* ModerationOptOutUnpublishNode constructor.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
|
||||
* The moderation information service.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModerationInformationInterface $moderation_info) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->moderationInfo = $moderation_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration, $plugin_id, $plugin_definition,
|
||||
$container->get('content_moderation.moderation_information')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function execute($entity = NULL) {
|
||||
if ($entity && $this->moderationInfo->isModeratedEntity($entity)) {
|
||||
drupal_set_message($this->t('One or more entities were skipped as they are under moderation and may not be directly published or unpublished.'));
|
||||
return;
|
||||
}
|
||||
|
||||
parent::execute($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
|
||||
$result = parent::access($object, $account, TRUE)
|
||||
->andif(AccessResult::forbiddenIf($this->moderationInfo->isModeratedEntity($object))->addCacheableDependency($object));
|
||||
|
||||
return $return_as_object ? $result : $result->isAllowed();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Derivative;
|
||||
|
||||
use Drupal\Component\Plugin\Derivative\DeriverBase;
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Generates moderation-related local tasks.
|
||||
*/
|
||||
class DynamicLocalTasks extends DeriverBase implements ContainerDeriverInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The base plugin ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $basePluginId;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* Creates an FieldUiLocalTask object.
|
||||
*
|
||||
* @param string $base_plugin_id
|
||||
* The base plugin ID.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
|
||||
* The translation manager.
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
|
||||
* The moderation information service.
|
||||
*/
|
||||
public function __construct($base_plugin_id, EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation, ModerationInformationInterface $moderation_information) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->stringTranslation = $string_translation;
|
||||
$this->basePluginId = $base_plugin_id;
|
||||
$this->moderationInfo = $moderation_information;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, $base_plugin_id) {
|
||||
return new static(
|
||||
$base_plugin_id,
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('string_translation'),
|
||||
$container->get('content_moderation.moderation_information')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||
$this->derivatives = [];
|
||||
|
||||
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
|
||||
if ($this->moderationInfo->canModerateEntitiesOfEntityType($entity_type)) {
|
||||
$bundle_id = $entity_type->getBundleEntityType();
|
||||
$this->derivatives["$bundle_id.moderation_tab"] = [
|
||||
'route_name' => "entity.$bundle_id.moderation",
|
||||
'title' => $this->t('Manage moderation'),
|
||||
// @todo - are we sure they all have an edit_form?
|
||||
'base_route' => "entity.$bundle_id.edit_form",
|
||||
'weight' => 30,
|
||||
] + $base_plugin_definition;
|
||||
}
|
||||
}
|
||||
|
||||
$latest_version_entities = array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $type) {
|
||||
return $this->moderationInfo->canModerateEntitiesOfEntityType($type) && $type->hasLinkTemplate('latest-version');
|
||||
});
|
||||
|
||||
foreach ($latest_version_entities as $entity_type_id => $entity_type) {
|
||||
$this->derivatives["$entity_type_id.latest_version_tab"] = [
|
||||
'route_name' => "entity.$entity_type_id.latest_version",
|
||||
'title' => $this->t('Latest version'),
|
||||
'base_route' => "entity.$entity_type_id.canonical",
|
||||
'weight' => 1,
|
||||
] + $base_plugin_definition;
|
||||
}
|
||||
|
||||
return $this->derivatives;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,255 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Field\FieldWidget;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\Query\QueryInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsSelectWidget;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\content_moderation\ModerationInformation;
|
||||
use Drupal\content_moderation\StateTransitionValidation;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Plugin implementation of the 'moderation_state_default' widget.
|
||||
*
|
||||
* @FieldWidget(
|
||||
* id = "moderation_state_default",
|
||||
* label = @Translation("Moderation state"),
|
||||
* field_types = {
|
||||
* "entity_reference"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class ModerationStateWidget extends OptionsSelectWidget implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* Current user service.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* Moderation state transition entity query.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\Query\QueryInterface
|
||||
*/
|
||||
protected $moderationStateTransitionEntityQuery;
|
||||
|
||||
/**
|
||||
* Moderation state storage.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityStorageInterface
|
||||
*/
|
||||
protected $moderationStateStorage;
|
||||
|
||||
/**
|
||||
* Moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformation
|
||||
*/
|
||||
protected $moderationInformation;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Moderation state transition storage.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityStorageInterface
|
||||
*/
|
||||
protected $moderationStateTransitionStorage;
|
||||
|
||||
/**
|
||||
* Moderation state transition validation service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\StateTransitionValidation
|
||||
*/
|
||||
protected $validator;
|
||||
|
||||
/**
|
||||
* Constructs a new ModerationStateWidget object.
|
||||
*
|
||||
* @param string $plugin_id
|
||||
* Plugin id.
|
||||
* @param mixed $plugin_definition
|
||||
* Plugin definition.
|
||||
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
|
||||
* Field definition.
|
||||
* @param array $settings
|
||||
* Field settings.
|
||||
* @param array $third_party_settings
|
||||
* Third party settings.
|
||||
* @param \Drupal\Core\Session\AccountInterface $current_user
|
||||
* Current user service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* Entity type manager.
|
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $moderation_state_storage
|
||||
* Moderation state storage.
|
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $moderation_state_transition_storage
|
||||
* Moderation state transition storage.
|
||||
* @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
|
||||
* Moderation transition entity query service.
|
||||
* @param \Drupal\content_moderation\ModerationInformation $moderation_information
|
||||
* Moderation information service.
|
||||
* @param \Drupal\content_moderation\StateTransitionValidation $validator
|
||||
* Moderation state transition validation service
|
||||
*/
|
||||
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, EntityStorageInterface $moderation_state_storage, EntityStorageInterface $moderation_state_transition_storage, QueryInterface $entity_query, ModerationInformation $moderation_information, StateTransitionValidation $validator) {
|
||||
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
|
||||
$this->moderationStateTransitionEntityQuery = $entity_query;
|
||||
$this->moderationStateTransitionStorage = $moderation_state_transition_storage;
|
||||
$this->moderationStateStorage = $moderation_state_storage;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->currentUser = $current_user;
|
||||
$this->moderationInformation = $moderation_information;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$configuration['field_definition'],
|
||||
$configuration['settings'],
|
||||
$configuration['third_party_settings'],
|
||||
$container->get('current_user'),
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('entity_type.manager')->getStorage('moderation_state'),
|
||||
$container->get('entity_type.manager')->getStorage('moderation_state_transition'),
|
||||
$container->get('entity.query')->get('moderation_state_transition', 'AND'),
|
||||
$container->get('content_moderation.moderation_information'),
|
||||
$container->get('content_moderation.state_transition_validation')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
|
||||
/** @var ContentEntityInterface $entity */
|
||||
$entity = $items->getEntity();
|
||||
|
||||
/* @var \Drupal\Core\Config\Entity\ConfigEntityInterface $bundle_entity */
|
||||
$bundle_entity = $this->entityTypeManager->getStorage($entity->getEntityType()->getBundleEntityType())->load($entity->bundle());
|
||||
if (!$this->moderationInformation->isModeratedEntity($entity)) {
|
||||
// @todo https://www.drupal.org/node/2779933 write a test for this.
|
||||
return $element + ['#access' => FALSE];
|
||||
}
|
||||
|
||||
$default = $items->get($delta)->value ?: $bundle_entity->getThirdPartySetting('content_moderation', 'default_moderation_state', FALSE);
|
||||
/** @var \Drupal\content_moderation\ModerationStateInterface $default_state */
|
||||
$default_state = $this->entityTypeManager->getStorage('moderation_state')->load($default);
|
||||
if (!$default || !$default_state) {
|
||||
throw new \UnexpectedValueException(sprintf('The %s bundle has an invalid moderation state configuration, moderation states are enabled but no default is set.', $bundle_entity->label()));
|
||||
}
|
||||
|
||||
$transitions = $this->validator->getValidTransitions($entity, $this->currentUser);
|
||||
|
||||
$target_states = [];
|
||||
/** @var \Drupal\content_moderation\Entity\ModerationStateTransition $transition */
|
||||
foreach ($transitions as $transition) {
|
||||
$target_states[$transition->getToState()] = $transition->label();
|
||||
}
|
||||
|
||||
// @todo https://www.drupal.org/node/2779933 write a test for this.
|
||||
$element += [
|
||||
'#access' => FALSE,
|
||||
'#type' => 'select',
|
||||
'#options' => $target_states,
|
||||
'#default_value' => $default,
|
||||
'#published' => $default ? $default_state->isPublishedState() : FALSE,
|
||||
'#key_column' => $this->column,
|
||||
];
|
||||
$element['#element_validate'][] = array(get_class($this), 'validateElement');
|
||||
|
||||
// Use the dropbutton.
|
||||
$element['#process'][] = [get_called_class(), 'processActions'];
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity builder updating the node moderation state with the submitted value.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type identifier.
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity updated with the submitted values.
|
||||
* @param array $form
|
||||
* The complete form array.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*/
|
||||
public static function updateStatus($entity_type_id, ContentEntityInterface $entity, array $form, FormStateInterface $form_state) {
|
||||
$element = $form_state->getTriggeringElement();
|
||||
if (isset($element['#moderation_state'])) {
|
||||
$entity->moderation_state->target_id = $element['#moderation_state'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process callback to alter action buttons.
|
||||
*/
|
||||
public static function processActions($element, FormStateInterface $form_state, array &$form) {
|
||||
|
||||
// We'll steal most of the button configuration from the default submit
|
||||
// button. However, NodeForm also hides that button for admins (as it adds
|
||||
// its own, too), so we have to restore it.
|
||||
$default_button = $form['actions']['submit'];
|
||||
$default_button['#access'] = TRUE;
|
||||
|
||||
// Add a custom button for each transition we're allowing. The #dropbutton
|
||||
// property tells FAPI to cluster them all together into a single widget.
|
||||
$options = $element['#options'];
|
||||
|
||||
$entity = $form_state->getFormObject()->getEntity();
|
||||
$translatable = !$entity->isNew() && $entity->isTranslatable();
|
||||
foreach ($options as $id => $label) {
|
||||
$button = [
|
||||
'#dropbutton' => 'save',
|
||||
'#moderation_state' => $id,
|
||||
'#weight' => -10,
|
||||
];
|
||||
|
||||
$button['#value'] = $translatable
|
||||
? t('Save and @transition (this translation)', ['@transition' => $label])
|
||||
: t('Save and @transition', ['@transition' => $label]);
|
||||
|
||||
$form['actions']['moderation_state_' . $id] = $button + $default_button;
|
||||
}
|
||||
|
||||
// Hide the default buttons, including the specialty ones added by
|
||||
// NodeForm.
|
||||
foreach (['publish', 'unpublish', 'submit'] as $key) {
|
||||
$form['actions'][$key]['#access'] = FALSE;
|
||||
unset($form['actions'][$key]['#dropbutton']);
|
||||
}
|
||||
|
||||
// Setup a callback to translate the button selection back into field
|
||||
// widget, so that it will get saved properly.
|
||||
$form['#entity_builders']['update_moderation_state'] = [get_called_class(), 'updateStatus'];
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function isApplicable(FieldDefinitionInterface $field_definition) {
|
||||
return parent::isApplicable($field_definition) && $field_definition->getName() === 'moderation_state';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Field;
|
||||
|
||||
use Drupal\content_moderation\Entity\ModerationState;
|
||||
use Drupal\Core\Field\EntityReferenceFieldItemList;
|
||||
|
||||
/**
|
||||
* A computed field that provides a content entity's moderation state.
|
||||
*
|
||||
* It links content entities to a moderation state configuration entity via a
|
||||
* moderation state content entity.
|
||||
*/
|
||||
class ModerationStateFieldItemList extends EntityReferenceFieldItemList {
|
||||
|
||||
/**
|
||||
* Gets the moderation state entity linked to a content entity revision.
|
||||
*
|
||||
* @return \Drupal\content_moderation\ModerationStateInterface|null
|
||||
* The moderation state configuration entity linked to a content entity
|
||||
* revision.
|
||||
*/
|
||||
protected function getModerationState() {
|
||||
$entity = $this->getEntity();
|
||||
|
||||
if (!\Drupal::service('content_moderation.moderation_information')->shouldModerateEntitiesOfBundle($entity->getEntityType(), $entity->bundle())) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if ($entity->id() && $entity->getRevisionId()) {
|
||||
$revisions = \Drupal::service('entity.query')->get('content_moderation_state')
|
||||
->condition('content_entity_type_id', $entity->getEntityTypeId())
|
||||
->condition('content_entity_id', $entity->id())
|
||||
->condition('content_entity_revision_id', $entity->getRevisionId())
|
||||
->allRevisions()
|
||||
->sort('revision_id', 'DESC')
|
||||
->execute();
|
||||
|
||||
if ($revision_to_load = key($revisions)) {
|
||||
/** @var \Drupal\content_moderation\ContentModerationStateInterface $content_moderation_state */
|
||||
$content_moderation_state = \Drupal::entityTypeManager()
|
||||
->getStorage('content_moderation_state')
|
||||
->loadRevision($revision_to_load);
|
||||
|
||||
// Return the correct translation.
|
||||
if ($entity->getEntityType()->hasKey('langcode')) {
|
||||
$langcode = $entity->language()->getId();
|
||||
if (!$content_moderation_state->hasTranslation($langcode)) {
|
||||
$content_moderation_state->addTranslation($langcode);
|
||||
}
|
||||
if ($content_moderation_state->language()->getId() !== $langcode) {
|
||||
$content_moderation_state = $content_moderation_state->getTranslation($langcode);
|
||||
}
|
||||
}
|
||||
|
||||
return $content_moderation_state->get('moderation_state')->entity;
|
||||
}
|
||||
}
|
||||
// It is possible that the bundle does not exist at this point. For example,
|
||||
// the node type form creates a fake Node entity to get default values.
|
||||
// @see \Drupal\node\NodeTypeForm::form()
|
||||
$bundle_entity = \Drupal::entityTypeManager()
|
||||
->getStorage($entity->getEntityType()->getBundleEntityType())
|
||||
->load($entity->bundle());
|
||||
if ($bundle_entity && ($default = $bundle_entity->getThirdPartySetting('content_moderation', 'default_moderation_state'))) {
|
||||
return ModerationState::load($default);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get($index) {
|
||||
if ($index !== 0) {
|
||||
throw new \InvalidArgumentException('An entity can not have multiple moderation states at the same time.');
|
||||
}
|
||||
$this->computeModerationFieldItemList();
|
||||
return isset($this->list[$index]) ? $this->list[$index] : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getIterator() {
|
||||
$this->computeModerationFieldItemList();
|
||||
return parent::getIterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate the moderation field item list.
|
||||
*/
|
||||
protected function computeModerationFieldItemList() {
|
||||
// Compute the value of the moderation state.
|
||||
$index = 0;
|
||||
if (!isset($this->list[$index]) || $this->list[$index]->isEmpty()) {
|
||||
$moderation_state = $this->getModerationState();
|
||||
// Do not store NULL values in the static cache.
|
||||
if ($moderation_state) {
|
||||
$this->list[$index] = $this->createItem($index, ['entity' => $moderation_state]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
104
web/core/modules/content_moderation/src/Plugin/Menu/EditTab.php
Normal file
104
web/core/modules/content_moderation/src/Plugin/Menu/EditTab.php
Normal file
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Menu;
|
||||
|
||||
use Drupal\Core\Menu\LocalTaskDefault;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||
use Drupal\content_moderation\ModerationInformation;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines a class for making the edit tab use 'Edit draft' or 'New draft'.
|
||||
*/
|
||||
class EditTab extends LocalTaskDefault implements ContainerFactoryPluginInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformation
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* The entity.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\ContentEntityInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* Constructs a new EditTab object.
|
||||
*
|
||||
* @param array $configuration
|
||||
* Plugin configuration.
|
||||
* @param string $plugin_id
|
||||
* Plugin ID.
|
||||
* @param mixed $plugin_definition
|
||||
* Plugin definition.
|
||||
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
|
||||
* The translation service.
|
||||
* @param \Drupal\content_moderation\ModerationInformation $moderation_information
|
||||
* The moderation information.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, TranslationInterface $string_translation, ModerationInformation $moderation_information) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
|
||||
$this->stringTranslation = $string_translation;
|
||||
$this->moderationInfo = $moderation_information;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('string_translation'),
|
||||
$container->get('content_moderation.moderation_information')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRouteParameters(RouteMatchInterface $route_match) {
|
||||
// Override the node here with the latest revision.
|
||||
$this->entity = $route_match->getParameter($this->pluginDefinition['entity_type_id']);
|
||||
return parent::getRouteParameters($route_match);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTitle() {
|
||||
if (!$this->moderationInfo->isModeratedEntity($this->entity)) {
|
||||
// Moderation isn't enabled.
|
||||
return parent::getTitle();
|
||||
}
|
||||
|
||||
// @todo https://www.drupal.org/node/2779933 write a test for this.
|
||||
return $this->moderationInfo->isLiveRevision($this->entity)
|
||||
? $this->t('New draft')
|
||||
: $this->t('Edit draft');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheTags() {
|
||||
// @todo https://www.drupal.org/node/2779933 write a test for this.
|
||||
$tags = parent::getCacheTags();
|
||||
// Tab changes if node or node-type is modified.
|
||||
$tags = array_merge($tags, $this->entity->getCacheTags());
|
||||
$tags[] = $this->entity->getEntityType()->getBundleEntityType() . ':' . $this->entity->bundle();
|
||||
return $tags;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Validation\Constraint;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* Verifies that nodes have a valid moderation state.
|
||||
*
|
||||
* @Constraint(
|
||||
* id = "ModerationState",
|
||||
* label = @Translation("Valid moderation state", context = "Validation")
|
||||
* )
|
||||
*/
|
||||
class ModerationStateConstraint extends Constraint {
|
||||
|
||||
public $message = 'Invalid state transition from %from to %to';
|
||||
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\content_moderation\Entity\ModerationState as ModerationStateEntity;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Drupal\content_moderation\StateTransitionValidation;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
/**
|
||||
* Checks if a moderation state transition is valid.
|
||||
*/
|
||||
class ModerationStateConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The state transition validation.
|
||||
*
|
||||
* @var \Drupal\content_moderation\StateTransitionValidation
|
||||
*/
|
||||
protected $validation;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
private $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The moderation info.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInformation;
|
||||
|
||||
/**
|
||||
* Creates a new ModerationStateConstraintValidator instance.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\content_moderation\StateTransitionValidation $validation
|
||||
* The state transition validation.
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
|
||||
* The moderation information.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, StateTransitionValidation $validation, ModerationInformationInterface $moderation_information) {
|
||||
$this->validation = $validation;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->moderationInformation = $moderation_information;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('content_moderation.state_transition_validation'),
|
||||
$container->get('content_moderation.moderation_information')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value, Constraint $constraint) {
|
||||
/** @var \Drupal\Core\Entity\EntityInterface $entity */
|
||||
$entity = $value->getEntity();
|
||||
|
||||
// Ignore entities that are not subject to moderation anyway.
|
||||
if (!$this->moderationInformation->isModeratedEntity($entity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore entities that are being created for the first time.
|
||||
if ($entity->isNew()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore entities that are being moderated for the first time, such as
|
||||
// when they existed before moderation was enabled for this entity type.
|
||||
if ($this->isFirstTimeModeration($entity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$original_entity = $this->moderationInformation->getLatestRevision($entity->getEntityTypeId(), $entity->id());
|
||||
if (!$entity->isDefaultTranslation() && $original_entity->hasTranslation($entity->language()->getId())) {
|
||||
$original_entity = $original_entity->getTranslation($entity->language()->getId());
|
||||
}
|
||||
|
||||
if ($entity->moderation_state->target_id) {
|
||||
$new_state_id = $entity->moderation_state->target_id;
|
||||
}
|
||||
else {
|
||||
$new_state_id = $default = $this->entityTypeManager
|
||||
->getStorage($entity->getEntityType()->getBundleEntityType())->load($entity->bundle())
|
||||
->getThirdPartySetting('content_moderation', 'default_moderation_state');
|
||||
}
|
||||
if ($new_state_id) {
|
||||
$new_state = ModerationStateEntity::load($new_state_id);
|
||||
}
|
||||
// @todo - what if $new_state_id references something that does not exist or
|
||||
// is null.
|
||||
if (!$this->validation->isTransitionAllowed($original_entity->moderation_state->entity, $new_state)) {
|
||||
$this->context->addViolation($constraint->message, ['%from' => $original_entity->moderation_state->entity->label(), '%to' => $new_state->label()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this entity is being moderated for the first time.
|
||||
*
|
||||
* If the previous version of the entity has no moderation state, we assume
|
||||
* that means it predates the presence of moderation states.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being moderated.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if this is the entity's first time being moderated, FALSE otherwise.
|
||||
*/
|
||||
protected function isFirstTimeModeration(EntityInterface $entity) {
|
||||
$original_entity = $this->moderationInformation->getLatestRevision($entity->getEntityTypeId(), $entity->id());
|
||||
|
||||
$original_id = $original_entity->moderation_state->target_id;
|
||||
|
||||
return !($entity->moderation_state->target_id && $original_entity && $original_id);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\views\filter;
|
||||
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\views\Plugin\views\filter\FilterPluginBase;
|
||||
use Drupal\views\Plugin\ViewsHandlerManager;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Filter to show only the latest revision of an entity.
|
||||
*
|
||||
* @ingroup views_filter_handlers
|
||||
*
|
||||
* @ViewsFilter("latest_revision")
|
||||
*/
|
||||
class LatestRevision extends FilterPluginBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* Entity Type Manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Views Handler Plugin Manager.
|
||||
*
|
||||
* @var \Drupal\views\Plugin\ViewsHandlerManager
|
||||
*/
|
||||
protected $joinHandler;
|
||||
|
||||
/**
|
||||
* Database Connection.
|
||||
*
|
||||
* @var \Drupal\Core\Database\Connection
|
||||
*/
|
||||
protected $connection;
|
||||
|
||||
/**
|
||||
* Constructs a new LatestRevision.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* Entity Type Manager Service.
|
||||
* @param \Drupal\views\Plugin\ViewsHandlerManager $join_handler
|
||||
* Views Handler Plugin Manager.
|
||||
* @param \Drupal\Core\Database\Connection $connection
|
||||
* Database Connection.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ViewsHandlerManager $join_handler, Connection $connection) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->joinHandler = $join_handler;
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration, $plugin_id, $plugin_definition,
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('plugin.manager.views.join'),
|
||||
$container->get('database')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function adminSummary() {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function operatorForm(&$form, FormStateInterface $form_state) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function canExpose() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function query() {
|
||||
// The table doesn't exist until a moderated node has been saved at least
|
||||
// once. Just in case, disable this filter until then. Note that this means
|
||||
// the view will still show all revisions, not just latest, but this is
|
||||
// sufficiently edge-case-y that it's probably not worth the time to
|
||||
// handle more robustly.
|
||||
if (!$this->connection->schema()->tableExists('content_revision_tracker')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$table = $this->ensureMyTable();
|
||||
|
||||
/** @var \Drupal\views\Plugin\views\query\Sql $query */
|
||||
$query = $this->query;
|
||||
|
||||
$definition = $this->entityTypeManager->getDefinition($this->getEntityType());
|
||||
$keys = $definition->getKeys();
|
||||
|
||||
$definition = [
|
||||
'table' => 'content_revision_tracker',
|
||||
'type' => 'INNER',
|
||||
'field' => 'entity_id',
|
||||
'left_table' => $table,
|
||||
'left_field' => $keys['id'],
|
||||
'extra' => [
|
||||
['left_field' => $keys['langcode'], 'field' => 'langcode'],
|
||||
['left_field' => $keys['revision'], 'field' => 'revision_id'],
|
||||
['field' => 'entity_type', 'value' => $this->getEntityType()],
|
||||
],
|
||||
];
|
||||
|
||||
$join = $this->joinHandler->createInstance('standard', $definition);
|
||||
|
||||
$query->ensureTable('content_revision_tracker', $this->relationship, $join);
|
||||
}
|
||||
|
||||
}
|
152
web/core/modules/content_moderation/src/RevisionTracker.php
Normal file
152
web/core/modules/content_moderation/src/RevisionTracker.php
Normal file
|
@ -0,0 +1,152 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Database\DatabaseExceptionWrapper;
|
||||
use Drupal\Core\Database\SchemaObjectExistsException;
|
||||
|
||||
/**
|
||||
* Tracks metadata about revisions across entities.
|
||||
*/
|
||||
class RevisionTracker implements RevisionTrackerInterface {
|
||||
|
||||
/**
|
||||
* The name of the SQL table we use for tracking.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tableName;
|
||||
|
||||
/**
|
||||
* The database connection.
|
||||
*
|
||||
* @var \Drupal\Core\Database\Connection
|
||||
*/
|
||||
protected $connection;
|
||||
|
||||
/**
|
||||
* Constructs a new RevisionTracker.
|
||||
*
|
||||
* @param \Drupal\Core\Database\Connection $connection
|
||||
* The database connection.
|
||||
* @param string $table
|
||||
* The table that should be used for tracking.
|
||||
*/
|
||||
public function __construct(Connection $connection, $table = 'content_revision_tracker') {
|
||||
$this->connection = $connection;
|
||||
$this->tableName = $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id) {
|
||||
try {
|
||||
$this->recordLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id);
|
||||
}
|
||||
catch (DatabaseExceptionWrapper $e) {
|
||||
$this->ensureTableExists();
|
||||
$this->recordLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the latest revision of a given entity.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The machine name of the type of entity.
|
||||
* @param string $entity_id
|
||||
* The Entity ID in question.
|
||||
* @param string $langcode
|
||||
* The langcode of the revision we're saving. Each language has its own
|
||||
* effective tree of entity revisions, so in different languages
|
||||
* different revisions will be "latest".
|
||||
* @param int $revision_id
|
||||
* The revision ID that is now the latest revision.
|
||||
*
|
||||
* @return int
|
||||
* One of the valid returns from a merge query's execute method.
|
||||
*/
|
||||
protected function recordLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id) {
|
||||
return $this->connection->merge($this->tableName)
|
||||
->keys([
|
||||
'entity_type' => $entity_type_id,
|
||||
'entity_id' => $entity_id,
|
||||
'langcode' => $langcode,
|
||||
])
|
||||
->fields([
|
||||
'revision_id' => $revision_id,
|
||||
])
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the table exists and create it if not.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the table was created, FALSE otherwise.
|
||||
*/
|
||||
protected function ensureTableExists() {
|
||||
try {
|
||||
if (!$this->connection->schema()->tableExists($this->tableName)) {
|
||||
$this->connection->schema()->createTable($this->tableName, $this->schemaDefinition());
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
catch (SchemaObjectExistsException $e) {
|
||||
// If another process has already created the table, attempting to
|
||||
// recreate it will throw an exception. In this case just catch the
|
||||
// exception and do nothing.
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the schema for the tracker table.
|
||||
*
|
||||
* @return array
|
||||
* The schema API definition for the SQL storage table.
|
||||
*/
|
||||
protected function schemaDefinition() {
|
||||
$schema = [
|
||||
'description' => 'Tracks the latest revision for any entity',
|
||||
'fields' => [
|
||||
'entity_type' => [
|
||||
'description' => 'The entity type',
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
],
|
||||
'entity_id' => [
|
||||
'description' => 'The entity ID',
|
||||
'type' => 'int',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
],
|
||||
'langcode' => [
|
||||
'description' => 'The language of the entity revision',
|
||||
'type' => 'varchar',
|
||||
'length' => 12,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
],
|
||||
'revision_id' => [
|
||||
'description' => 'The latest revision ID for this entity',
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
],
|
||||
],
|
||||
'primary key' => ['entity_type', 'entity_id', 'langcode'],
|
||||
];
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
/**
|
||||
* Tracks metadata about revisions across content entities.
|
||||
*/
|
||||
interface RevisionTrackerInterface {
|
||||
|
||||
/**
|
||||
* Sets the latest revision of a given entity.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The machine name of the type of entity.
|
||||
* @param string $entity_id
|
||||
* The Entity ID in question.
|
||||
* @param string $langcode
|
||||
* The langcode of the revision we're saving. Each language has its own
|
||||
* effective tree of entity revisions, so in different languages
|
||||
* different revisions will be "latest".
|
||||
* @param int $revision_id
|
||||
* The revision ID that is now the latest revision.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function setLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id);
|
||||
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Routing;
|
||||
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityHandlerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Dynamic route provider for the Content moderation module.
|
||||
*
|
||||
* Provides the following routes:
|
||||
* - The latest version tab, showing the latest revision of an entity, not the
|
||||
* default one.
|
||||
*/
|
||||
class EntityModerationRouteProvider implements EntityRouteProviderInterface, EntityHandlerInterface {
|
||||
|
||||
/**
|
||||
* The entity manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
|
||||
*/
|
||||
protected $entityFieldManager;
|
||||
|
||||
/**
|
||||
* Constructs a new DefaultHtmlRouteProvider.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_manager
|
||||
* The entity manager.
|
||||
*/
|
||||
public function __construct(EntityFieldManagerInterface $entity_manager) {
|
||||
$this->entityFieldManager = $entity_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
return new static(
|
||||
$container->get('entity_field.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRoutes(EntityTypeInterface $entity_type) {
|
||||
$collection = new RouteCollection();
|
||||
|
||||
if ($moderation_route = $this->getLatestVersionRoute($entity_type)) {
|
||||
$entity_type_id = $entity_type->id();
|
||||
$collection->add("entity.{$entity_type_id}.latest_version", $moderation_route);
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the moderation-form route.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type.
|
||||
*
|
||||
* @return \Symfony\Component\Routing\Route|null
|
||||
* The generated route, if available.
|
||||
*/
|
||||
protected function getLatestVersionRoute(EntityTypeInterface $entity_type) {
|
||||
if ($entity_type->hasLinkTemplate('latest-version') && $entity_type->hasViewBuilderClass()) {
|
||||
$entity_type_id = $entity_type->id();
|
||||
$route = new Route($entity_type->getLinkTemplate('latest-version'));
|
||||
$route
|
||||
->addDefaults([
|
||||
'_entity_view' => "{$entity_type_id}.full",
|
||||
'_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::title',
|
||||
])
|
||||
// If the entity type is a node, unpublished content will be visible
|
||||
// if the user has the "view all unpublished content" permission.
|
||||
->setRequirement('_entity_access', "{$entity_type_id}.view")
|
||||
->setRequirement('_permission', 'view latest version,view any unpublished content')
|
||||
->setRequirement('_content_moderation_latest_version', 'TRUE')
|
||||
->setOption('_content_moderation_entity_type', $entity_type_id)
|
||||
->setOption('parameters', [
|
||||
$entity_type_id => [
|
||||
'type' => 'entity:' . $entity_type_id,
|
||||
'load_forward_revision' => 1,
|
||||
],
|
||||
]);
|
||||
|
||||
// Entity types with serial IDs can specify this in their route
|
||||
// requirements, improving the matching process.
|
||||
if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') {
|
||||
$route->setRequirement($entity_type_id, '\d+');
|
||||
}
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type of the ID key for a given entity type.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* An entity type.
|
||||
*
|
||||
* @return string|null
|
||||
* The type of the ID key for a given entity type, or NULL if the entity
|
||||
* type does not support fields.
|
||||
*/
|
||||
protected function getEntityTypeIdKeyType(EntityTypeInterface $entity_type) {
|
||||
if (!$entity_type->isSubclassOf(FieldableEntityInterface::class)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id());
|
||||
return $field_storage_definitions[$entity_type->getKey('id')]->getType();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Routing;
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Provides the moderation configuration routes for config entities.
|
||||
*/
|
||||
class EntityTypeModerationRouteProvider implements EntityRouteProviderInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRoutes(EntityTypeInterface $entity_type) {
|
||||
$collection = new RouteCollection();
|
||||
|
||||
if ($moderation_route = $this->getModerationFormRoute($entity_type)) {
|
||||
$entity_type_id = $entity_type->id();
|
||||
$collection->add("entity.{$entity_type_id}.moderation", $moderation_route);
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the moderation-form route.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type.
|
||||
*
|
||||
* @return \Symfony\Component\Routing\Route|null
|
||||
* The generated route, if available.
|
||||
*/
|
||||
protected function getModerationFormRoute(EntityTypeInterface $entity_type) {
|
||||
if ($entity_type->hasLinkTemplate('moderation-form') && $entity_type->getFormClass('moderation')) {
|
||||
$entity_type_id = $entity_type->id();
|
||||
|
||||
$route = new Route($entity_type->getLinkTemplate('moderation-form'));
|
||||
|
||||
// @todo Come up with a new permission.
|
||||
$route
|
||||
->setDefaults([
|
||||
'_entity_form' => "{$entity_type_id}.moderation",
|
||||
'_title' => 'Moderation',
|
||||
])
|
||||
->setRequirement('_permission', 'administer moderation states')
|
||||
->setOption('parameters', [
|
||||
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
|
||||
]);
|
||||
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\Query\QueryFactory;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\content_moderation\Entity\ModerationStateTransition;
|
||||
|
||||
/**
|
||||
* Validates whether a certain state transition is allowed.
|
||||
*/
|
||||
class StateTransitionValidation implements StateTransitionValidationInterface {
|
||||
|
||||
/**
|
||||
* Entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Entity query factory.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\Query\QueryFactory
|
||||
*/
|
||||
protected $queryFactory;
|
||||
|
||||
/**
|
||||
* Stores the possible state transitions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $possibleTransitions = [];
|
||||
|
||||
/**
|
||||
* Constructs a new StateTransitionValidation.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager service.
|
||||
* @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
|
||||
* The entity query factory.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, QueryFactory $query_factory) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->queryFactory = $query_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a mapping of possible transitions.
|
||||
*
|
||||
* This method is uncached and will recalculate the list on every request.
|
||||
* In most cases you want to use getPossibleTransitions() instead.
|
||||
*
|
||||
* @see static::getPossibleTransitions()
|
||||
*
|
||||
* @return array[]
|
||||
* An array containing all possible transitions. Each entry is keyed by the
|
||||
* "from" state, and the value is an array of all legal "to" states based
|
||||
* on the currently defined transition objects.
|
||||
*/
|
||||
protected function calculatePossibleTransitions() {
|
||||
$transitions = $this->transitionStorage()->loadMultiple();
|
||||
|
||||
$possible_transitions = [];
|
||||
/** @var \Drupal\content_moderation\ModerationStateTransitionInterface $transition */
|
||||
foreach ($transitions as $transition) {
|
||||
$possible_transitions[$transition->getFromState()][] = $transition->getToState();
|
||||
}
|
||||
return $possible_transitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mapping of possible transitions.
|
||||
*
|
||||
* @return array[]
|
||||
* An array containing all possible transitions. Each entry is keyed by the
|
||||
* "from" state, and the value is an array of all legal "to" states based
|
||||
* on the currently defined transition objects.
|
||||
*/
|
||||
protected function getPossibleTransitions() {
|
||||
if (empty($this->possibleTransitions)) {
|
||||
$this->possibleTransitions = $this->calculatePossibleTransitions();
|
||||
}
|
||||
return $this->possibleTransitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getValidTransitionTargets(ContentEntityInterface $entity, AccountInterface $user) {
|
||||
$bundle = $this->loadBundleEntity($entity->getEntityType()->getBundleEntityType(), $entity->bundle());
|
||||
|
||||
$states_for_bundle = $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', []);
|
||||
|
||||
/** @var \Drupal\content_moderation\Entity\ModerationState $current_state */
|
||||
$current_state = $entity->moderation_state->entity;
|
||||
|
||||
$all_transitions = $this->getPossibleTransitions();
|
||||
$destination_ids = $all_transitions[$current_state->id()];
|
||||
|
||||
$destination_ids = array_intersect($states_for_bundle, $destination_ids);
|
||||
$destinations = $this->entityTypeManager->getStorage('moderation_state')->loadMultiple($destination_ids);
|
||||
|
||||
return array_filter($destinations, function(ModerationStateInterface $destination_state) use ($current_state, $user) {
|
||||
return $this->userMayTransition($current_state, $destination_state, $user);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user) {
|
||||
$bundle = $this->loadBundleEntity($entity->getEntityType()->getBundleEntityType(), $entity->bundle());
|
||||
|
||||
/** @var \Drupal\content_moderation\Entity\ModerationState $current_state */
|
||||
$current_state = $entity->moderation_state->entity;
|
||||
$current_state_id = $current_state ? $current_state->id() : $bundle->getThirdPartySetting('content_moderation', 'default_moderation_state');
|
||||
|
||||
// Determine the states that are legal on this bundle.
|
||||
$legal_bundle_states = $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', []);
|
||||
|
||||
// Legal transitions include those that are possible from the current state,
|
||||
// filtered by those whose target is legal on this bundle and that the
|
||||
// user has access to execute.
|
||||
$transitions = array_filter($this->getTransitionsFrom($current_state_id), function(ModerationStateTransition $transition) use ($legal_bundle_states, $user) {
|
||||
return in_array($transition->getToState(), $legal_bundle_states, TRUE)
|
||||
&& $user->hasPermission('use ' . $transition->id() . ' transition');
|
||||
});
|
||||
|
||||
return $transitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of possible transitions from a given state.
|
||||
*
|
||||
* This list is based only on those transitions that exist, not what
|
||||
* transitions are legal in a given context.
|
||||
*
|
||||
* @param string $state_name
|
||||
* The machine name of the state from which we are transitioning.
|
||||
*
|
||||
* @return ModerationStateTransition[]
|
||||
* A list of possible transitions from a given state.
|
||||
*/
|
||||
protected function getTransitionsFrom($state_name) {
|
||||
$result = $this->transitionStateQuery()
|
||||
->condition('stateFrom', $state_name)
|
||||
->sort('weight')
|
||||
->execute();
|
||||
|
||||
return $this->transitionStorage()->loadMultiple($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function userMayTransition(ModerationStateInterface $from, ModerationStateInterface $to, AccountInterface $user) {
|
||||
if ($transition = $this->getTransitionFromStates($from, $to)) {
|
||||
return $user->hasPermission('use ' . $transition->id() . ' transition');
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the transition object that transitions from one state to another.
|
||||
*
|
||||
* @param \Drupal\content_moderation\ModerationStateInterface $from
|
||||
* The origin state.
|
||||
* @param \Drupal\content_moderation\ModerationStateInterface $to
|
||||
* The destination state.
|
||||
*
|
||||
* @return ModerationStateTransition|null
|
||||
* A transition object, or NULL if there is no such transition.
|
||||
*/
|
||||
protected function getTransitionFromStates(ModerationStateInterface $from, ModerationStateInterface $to) {
|
||||
$from = $this->transitionStateQuery()
|
||||
->condition('stateFrom', $from->id())
|
||||
->condition('stateTo', $to->id())
|
||||
->execute();
|
||||
|
||||
$transitions = $this->transitionStorage()->loadMultiple($from);
|
||||
|
||||
if ($transitions) {
|
||||
return current($transitions);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isTransitionAllowed(ModerationStateInterface $from, ModerationStateInterface $to) {
|
||||
$allowed_transitions = $this->calculatePossibleTransitions();
|
||||
if (isset($allowed_transitions[$from->id()])) {
|
||||
return in_array($to->id(), $allowed_transitions[$from->id()], TRUE);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a transition state entity query.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\Query\QueryInterface
|
||||
* A transition state entity query.
|
||||
*/
|
||||
protected function transitionStateQuery() {
|
||||
return $this->queryFactory->get('moderation_state_transition', 'AND');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the transition entity storage service.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityStorageInterface
|
||||
* The transition state entity storage.
|
||||
*/
|
||||
protected function transitionStorage() {
|
||||
return $this->entityTypeManager->getStorage('moderation_state_transition');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state entity storage service.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityStorageInterface
|
||||
* The moderation state entity storage.
|
||||
*/
|
||||
protected function stateStorage() {
|
||||
return $this->entityTypeManager->getStorage('moderation_state');
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a specific bundle entity.
|
||||
*
|
||||
* @param string $bundle_entity_type_id
|
||||
* The bundle entity type ID.
|
||||
* @param string $bundle_id
|
||||
* The bundle ID.
|
||||
*
|
||||
* @return \Drupal\Core\Config\Entity\ConfigEntityInterface|null
|
||||
* The specific bundle entity.
|
||||
*/
|
||||
protected function loadBundleEntity($bundle_entity_type_id, $bundle_id) {
|
||||
return $this->entityTypeManager->getStorage($bundle_entity_type_id)->load($bundle_id);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
|
||||
/**
|
||||
* Validates whether a certain state transition is allowed.
|
||||
*/
|
||||
interface StateTransitionValidationInterface {
|
||||
|
||||
/**
|
||||
* Gets a list of states a user may transition an entity to.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity to be transitioned.
|
||||
* @param \Drupal\Core\Session\AccountInterface $user
|
||||
* The account that wants to perform a transition.
|
||||
*
|
||||
* @return \Drupal\content_moderation\Entity\ModerationState[]
|
||||
* Returns an array of States to which the specified user may transition the
|
||||
* entity.
|
||||
*/
|
||||
public function getValidTransitionTargets(ContentEntityInterface $entity, AccountInterface $user);
|
||||
|
||||
/**
|
||||
* Gets a list of transitions that are legal for this user on this entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity to be transitioned.
|
||||
* @param \Drupal\Core\Session\AccountInterface $user
|
||||
* The account that wants to perform a transition.
|
||||
*
|
||||
* @return \Drupal\content_moderation\Entity\ModerationStateTransition[]
|
||||
* The list of transitions that are legal for this user on this entity.
|
||||
*/
|
||||
public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user);
|
||||
|
||||
/**
|
||||
* Determines if a user is allowed to transition from one state to another.
|
||||
*
|
||||
* This method will also return FALSE if there is no transition between the
|
||||
* specified states at all.
|
||||
*
|
||||
* @param \Drupal\content_moderation\ModerationStateInterface $from
|
||||
* The origin state.
|
||||
* @param \Drupal\content_moderation\ModerationStateInterface $to
|
||||
* The destination state.
|
||||
* @param \Drupal\Core\Session\AccountInterface $user
|
||||
* The user to validate.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the given user may transition between those two states.
|
||||
*/
|
||||
public function userMayTransition(ModerationStateInterface $from, ModerationStateInterface $to, AccountInterface $user);
|
||||
|
||||
/**
|
||||
* Determines a transition allowed.
|
||||
*
|
||||
* @param \Drupal\content_moderation\ModerationStateInterface $from
|
||||
* The origin state.
|
||||
* @param \Drupal\content_moderation\ModerationStateInterface $to
|
||||
* The destination state.
|
||||
*
|
||||
* @return bool
|
||||
* Is the transition allowed.
|
||||
*/
|
||||
public function isTransitionAllowed(ModerationStateInterface $from, ModerationStateInterface $to);
|
||||
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
/**
|
||||
* Tests the moderation form, specifically on nodes.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationFormTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE, [
|
||||
'draft',
|
||||
'published',
|
||||
], 'draft');
|
||||
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the moderation form that shows on the latest version page.
|
||||
*
|
||||
* The latest version page only shows if there is a forward revision. There
|
||||
* is only a forward revision if a draft revision is created on a node where
|
||||
* the default revision is not a published moderation state.
|
||||
*
|
||||
* @see \Drupal\content_moderation\EntityOperations
|
||||
* @see \Drupal\content_moderation\Tests\ModerationStateBlockTest::testCustomBlockModeration
|
||||
*/
|
||||
public function testModerationForm() {
|
||||
// Create new moderated content in draft.
|
||||
$this->drupalPostForm('node/add/moderated_content', [
|
||||
'title[0][value]' => 'Some moderated content',
|
||||
'body[0][value]' => 'First version of the content.',
|
||||
], t('Save and Create New Draft'));
|
||||
|
||||
$node = $this->drupalGetNodeByTitle('Some moderated content');
|
||||
$canonical_path = sprintf('node/%d', $node->id());
|
||||
$edit_path = sprintf('node/%d/edit', $node->id());
|
||||
$latest_version_path = sprintf('node/%d/latest', $node->id());
|
||||
|
||||
$this->assertTrue($this->adminUser->hasPermission('edit any moderated_content content'));
|
||||
|
||||
// The latest version page should not show, because there is no forward
|
||||
// revision.
|
||||
$this->drupalGet($latest_version_path);
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Update the draft.
|
||||
$this->drupalPostForm($edit_path, [
|
||||
'body[0][value]' => 'Second version of the content.',
|
||||
], t('Save and Create New Draft'));
|
||||
|
||||
// The latest version page should not show, because there is still no
|
||||
// forward revision.
|
||||
$this->drupalGet($latest_version_path);
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Publish the draft.
|
||||
$this->drupalPostForm($edit_path, [
|
||||
'body[0][value]' => 'Third version of the content.',
|
||||
], t('Save and Publish'));
|
||||
|
||||
// The published view should not have a moderation form, because it is the
|
||||
// default revision.
|
||||
$this->drupalGet($canonical_path);
|
||||
$this->assertResponse(200);
|
||||
$this->assertNoText('Status', 'The node view page has no moderation form.');
|
||||
|
||||
// The latest version page should not show, because there is still no
|
||||
// forward revision.
|
||||
$this->drupalGet($latest_version_path);
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Make a forward revision.
|
||||
$this->drupalPostForm($edit_path, [
|
||||
'body[0][value]' => 'Fourth version of the content.',
|
||||
], t('Save and Create New Draft'));
|
||||
|
||||
// The published view should not have a moderation form, because it is the
|
||||
// default revision.
|
||||
$this->drupalGet($canonical_path);
|
||||
$this->assertResponse(200);
|
||||
$this->assertNoText('Status', 'The node view page has no moderation form.');
|
||||
|
||||
// The latest version page should show the moderation form and have "Draft"
|
||||
// status, because the forward revision is in "Draft".
|
||||
$this->drupalGet($latest_version_path);
|
||||
$this->assertResponse(200);
|
||||
$this->assertText('Status', 'Form text found on the latest-version page.');
|
||||
$this->assertText('Draft', 'Correct status found on the latest-version page.');
|
||||
|
||||
// Submit the moderation form to change status to published.
|
||||
$this->drupalPostForm($latest_version_path, [
|
||||
'new_state' => 'published',
|
||||
], t('Apply'));
|
||||
|
||||
// The latest version page should not show, because there is no
|
||||
// forward revision.
|
||||
$this->drupalGet($latest_version_path);
|
||||
$this->assertResponse(403);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
/**
|
||||
* Test content_moderation functionality with localization and translation.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationLocaleTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'node',
|
||||
'content_moderation',
|
||||
'locale',
|
||||
'content_translation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Tests article translations can be moderated separately.
|
||||
*/
|
||||
public function testTranslateModeratedContent() {
|
||||
$this->drupalLogin($this->rootUser);
|
||||
|
||||
// Enable moderation on Article node type.
|
||||
$this->createContentTypeFromUi(
|
||||
'Article',
|
||||
'article',
|
||||
TRUE,
|
||||
['draft', 'published', 'archived'],
|
||||
'draft'
|
||||
);
|
||||
|
||||
// Add French language.
|
||||
$edit = [
|
||||
'predefined_langcode' => 'fr',
|
||||
];
|
||||
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
|
||||
|
||||
// Enable content translation on articles.
|
||||
$this->drupalGet('admin/config/regional/content-language');
|
||||
$edit = [
|
||||
'entity_types[node]' => TRUE,
|
||||
'settings[node][article][translatable]' => TRUE,
|
||||
'settings[node][article][settings][language][language_alterable]' => TRUE,
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
|
||||
|
||||
// Adding languages requires a container rebuild in the test running
|
||||
// environment so that multilingual services are used.
|
||||
$this->rebuildContainer();
|
||||
|
||||
// Create a published article in English.
|
||||
$edit = [
|
||||
'title[0][value]' => 'Published English node',
|
||||
'langcode[0][value]' => 'en',
|
||||
];
|
||||
$this->drupalPostForm('node/add/article', $edit, t('Save and Publish'));
|
||||
$this->assertText(t('Article Published English node has been created.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Published English node');
|
||||
|
||||
// Add a French translation.
|
||||
$this->drupalGet('node/' . $english_node->id() . '/translations');
|
||||
$this->clickLink(t('Add'));
|
||||
$edit = [
|
||||
'title[0][value]' => 'French node Draft',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save and Create New Draft (this translation)'));
|
||||
// Here the error has occurred "The website encountered an unexpected error.
|
||||
// Please try again later."
|
||||
// If the translation has got lost.
|
||||
$this->assertText(t('Article French node Draft has been updated.'));
|
||||
|
||||
// Create an article in English.
|
||||
$edit = [
|
||||
'title[0][value]' => 'English node',
|
||||
'langcode[0][value]' => 'en',
|
||||
];
|
||||
$this->drupalPostForm('node/add/article', $edit, t('Save and Create New Draft'));
|
||||
$this->assertText(t('Article English node has been created.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('English node');
|
||||
|
||||
// Add a French translation.
|
||||
$this->drupalGet('node/' . $english_node->id() . '/translations');
|
||||
$this->clickLink(t('Add'));
|
||||
$edit = [
|
||||
'title[0][value]' => 'French node',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save and Create New Draft (this translation)'));
|
||||
$this->assertText(t('Article French node has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('English node', TRUE);
|
||||
|
||||
// Publish the English article and check that the translation stays
|
||||
// unpublished.
|
||||
$this->drupalPostForm('node/' . $english_node->id() . '/edit', [], t('Save and Publish (this translation)'));
|
||||
$this->assertText(t('Article English node has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('English node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertEqual('French node', $french_node->label());
|
||||
|
||||
$this->assertEqual($english_node->moderation_state->target_id, 'published');
|
||||
$this->assertTrue($english_node->isPublished());
|
||||
$this->assertEqual($french_node->moderation_state->target_id, 'draft');
|
||||
$this->assertFalse($french_node->isPublished());
|
||||
|
||||
// Create another article with its translation. This time we will publish
|
||||
// the translation first.
|
||||
$edit = [
|
||||
'title[0][value]' => 'Another node',
|
||||
];
|
||||
$this->drupalPostForm('node/add/article', $edit, t('Save and Create New Draft'));
|
||||
$this->assertText(t('Article Another node has been created.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Another node');
|
||||
|
||||
// Add a French translation.
|
||||
$this->drupalGet('node/' . $english_node->id() . '/translations');
|
||||
$this->clickLink(t('Add'));
|
||||
$edit = [
|
||||
'title[0][value]' => 'Translated node',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save and Create New Draft (this translation)'));
|
||||
$this->assertText(t('Article Translated node has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
|
||||
|
||||
// Publish the translation and check that the source language version stays
|
||||
// unpublished.
|
||||
$this->drupalPostForm('fr/node/' . $english_node->id() . '/edit', [], t('Save and Publish (this translation)'));
|
||||
$this->assertText(t('Article Translated node has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertEqual($french_node->moderation_state->target_id, 'published');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertEqual($english_node->moderation_state->target_id, 'draft');
|
||||
$this->assertFalse($english_node->isPublished());
|
||||
|
||||
// Now check that we can create a new draft of the translation.
|
||||
$edit = [
|
||||
'title[0][value]' => 'New draft of translated node',
|
||||
];
|
||||
$this->drupalPostForm('fr/node/' . $english_node->id() . '/edit', $edit, t('Save and Create New Draft (this translation)'));
|
||||
$this->assertText(t('Article New draft of translated node has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertEqual($french_node->moderation_state->target_id, 'published');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertEqual($french_node->getTitle(), 'Translated node', 'The default revision of the published translation remains the same.');
|
||||
|
||||
// Publish the draft.
|
||||
$edit = [
|
||||
'new_state' => 'published',
|
||||
];
|
||||
$this->drupalPostForm('fr/node/' . $english_node->id() . '/latest', $edit, t('Apply'));
|
||||
$this->assertText(t('The moderation state has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertEqual($french_node->moderation_state->target_id, 'published');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertEqual($french_node->getTitle(), 'New draft of translated node', 'The draft has replaced the published revision.');
|
||||
|
||||
// Publish the English article before testing the archive transition.
|
||||
$this->drupalPostForm('node/' . $english_node->id() . '/edit', [], t('Save and Publish (this translation)'));
|
||||
$this->assertText(t('Article Another node has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
|
||||
$this->assertEqual($english_node->moderation_state->target_id, 'published');
|
||||
|
||||
// Archive the node and its translation.
|
||||
$this->drupalPostForm('node/' . $english_node->id() . '/edit', [], t('Save and Archive (this translation)'));
|
||||
$this->assertText(t('Article Another node has been updated.'));
|
||||
$this->drupalPostForm('fr/node/' . $english_node->id() . '/edit', [], t('Save and Archive (this translation)'));
|
||||
$this->assertText(t('Article New draft of translated node has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertEqual($english_node->moderation_state->target_id, 'archived');
|
||||
$this->assertFalse($english_node->isPublished());
|
||||
$this->assertEqual($french_node->moderation_state->target_id, 'archived');
|
||||
$this->assertFalse($french_node->isPublished());
|
||||
|
||||
// Create another article with its translation. This time publishing english
|
||||
// after creating a forward french revision.
|
||||
$edit = [
|
||||
'title[0][value]' => 'An english node',
|
||||
];
|
||||
$this->drupalPostForm('node/add/article', $edit, t('Save and Create New Draft'));
|
||||
$this->assertText(t('Article An english node has been created.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('An english node');
|
||||
$this->assertFalse($english_node->isPublished());
|
||||
|
||||
// Add a French translation.
|
||||
$this->drupalGet('node/' . $english_node->id() . '/translations');
|
||||
$this->clickLink(t('Add'));
|
||||
$edit = [
|
||||
'title[0][value]' => 'A french node',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save and Publish (this translation)'));
|
||||
$english_node = $this->drupalGetNodeByTitle('An english node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertFalse($english_node->isPublished());
|
||||
|
||||
// Create a forward revision
|
||||
$this->drupalPostForm('fr/node/' . $english_node->id() . '/edit', [], t('Save and Create New Draft (this translation)'));
|
||||
$english_node = $this->drupalGetNodeByTitle('An english node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertFalse($english_node->isPublished());
|
||||
|
||||
// Publish the english node and the default french node not the latest
|
||||
// french node should be used.
|
||||
$this->drupalPostForm('/node/' . $english_node->id() . '/edit', [], t('Save and Publish (this translation)'));
|
||||
$english_node = $this->drupalGetNodeByTitle('An english node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertTrue($english_node->isPublished());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
use Drupal\block_content\Entity\BlockContent;
|
||||
use Drupal\block_content\Entity\BlockContentType;
|
||||
|
||||
/**
|
||||
* Tests general content moderation workflow for blocks.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationStateBlockTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create the "basic" block type.
|
||||
$bundle = BlockContentType::create([
|
||||
'id' => 'basic',
|
||||
'label' => 'basic',
|
||||
'revision' => FALSE,
|
||||
]);
|
||||
$bundle->save();
|
||||
|
||||
// Add the body field to it.
|
||||
block_content_add_body_field($bundle->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests moderating custom blocks.
|
||||
*
|
||||
* Blocks and any non-node-type-entities do not have a concept of
|
||||
* "published". As such, we must use the "default revision" to know what is
|
||||
* going to be "published", i.e. visible to the user.
|
||||
*
|
||||
* The one exception is a block that has never been "published". When a block
|
||||
* is first created, it becomes the "default revision". For each edit of the
|
||||
* block after that, Content Moderation checks the "default revision" to
|
||||
* see if it is set to a published moderation state. If it is not, the entity
|
||||
* being saved will become the "default revision".
|
||||
*
|
||||
* The test below is intended, in part, to make this behavior clear.
|
||||
*
|
||||
* @see \Drupal\content_moderation\EntityOperations::entityPresave
|
||||
* @see \Drupal\content_moderation\Tests\ModerationFormTest::testModerationForm
|
||||
*/
|
||||
public function testCustomBlockModeration() {
|
||||
$this->drupalLogin($this->rootUser);
|
||||
|
||||
$this->drupalGet('admin/structure/block/block-content/types');
|
||||
$this->assertLinkByHref('admin/structure/block/block-content/manage/basic/moderation');
|
||||
$this->drupalGet('admin/structure/block/block-content/manage/basic');
|
||||
$this->assertLinkByHref('admin/structure/block/block-content/manage/basic/moderation');
|
||||
$this->drupalGet('admin/structure/block/block-content/manage/basic/moderation');
|
||||
|
||||
// Enable moderation for custom blocks at
|
||||
// admin/structure/block/block-content/manage/basic/moderation.
|
||||
$edit = [
|
||||
'enable_moderation_state' => TRUE,
|
||||
'allowed_moderation_states_unpublished[draft]' => TRUE,
|
||||
'allowed_moderation_states_published[published]' => TRUE,
|
||||
'default_moderation_state' => 'draft',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save'));
|
||||
$this->assertText(t('Your settings have been saved.'));
|
||||
|
||||
// Create a custom block at block/add and save it as draft.
|
||||
$body = 'Body of moderated block';
|
||||
$edit = [
|
||||
'info[0][value]' => 'Moderated block',
|
||||
'body[0][value]' => $body,
|
||||
];
|
||||
$this->drupalPostForm('block/add', $edit, t('Save and Create New Draft'));
|
||||
$this->assertText(t('basic Moderated block has been created.'));
|
||||
|
||||
// Place the block in the Sidebar First region.
|
||||
$instance = array(
|
||||
'id' => 'moderated_block',
|
||||
'settings[label]' => $edit['info[0][value]'],
|
||||
'region' => 'sidebar_first',
|
||||
);
|
||||
$block = BlockContent::load(1);
|
||||
$url = 'admin/structure/block/add/block_content:' . $block->uuid() . '/' . $this->config('system.theme')->get('default');
|
||||
$this->drupalPostForm($url, $instance, t('Save block'));
|
||||
|
||||
// Navigate to home page and check that the block is visible. It should be
|
||||
// visible because it is the default revision.
|
||||
$this->drupalGet('');
|
||||
$this->assertText($body);
|
||||
|
||||
// Update the block.
|
||||
$updated_body = 'This is the new body value';
|
||||
$edit = [
|
||||
'body[0][value]' => $updated_body,
|
||||
];
|
||||
$this->drupalPostForm('block/' . $block->id(), $edit, t('Save and Create New Draft'));
|
||||
$this->assertText(t('basic Moderated block has been updated.'));
|
||||
|
||||
// Navigate to the home page and check that the block shows the updated
|
||||
// content. It should show the updated content because the block's default
|
||||
// revision is not a published moderation state.
|
||||
$this->drupalGet('');
|
||||
$this->assertText($updated_body);
|
||||
|
||||
// Publish the block so we can create a forward revision.
|
||||
$this->drupalPostForm('block/' . $block->id(), [], t('Save and Publish'));
|
||||
|
||||
// Create a forward revision.
|
||||
$forward_revision_body = 'This is the forward revision body value';
|
||||
$edit = [
|
||||
'body[0][value]' => $forward_revision_body,
|
||||
];
|
||||
$this->drupalPostForm('block/' . $block->id(), $edit, t('Save and Create New Draft'));
|
||||
$this->assertText(t('basic Moderated block has been updated.'));
|
||||
|
||||
// Navigate to home page and check that the forward revision doesn't show,
|
||||
// since it should not be set as the default revision.
|
||||
$this->drupalGet('');
|
||||
$this->assertText($updated_body);
|
||||
|
||||
// Open the latest tab and publish the new draft.
|
||||
$edit = [
|
||||
'new_state' => 'published',
|
||||
];
|
||||
$this->drupalPostForm('block/' . $block->id() . '/latest', $edit, t('Apply'));
|
||||
$this->assertText(t('The moderation state has been updated.'));
|
||||
|
||||
// Navigate to home page and check that the forward revision is now the
|
||||
// default revision and therefore visible.
|
||||
$this->drupalGet('');
|
||||
$this->assertText($forward_revision_body);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\node\Entity\Node;
|
||||
|
||||
/**
|
||||
* Tests general content moderation workflow for nodes.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationStateNodeTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->createContentTypeFromUi(
|
||||
'Moderated content',
|
||||
'moderated_content',
|
||||
TRUE,
|
||||
['draft', 'needs_review', 'published'],
|
||||
'draft'
|
||||
);
|
||||
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests creating and deleting content.
|
||||
*/
|
||||
public function testCreatingContent() {
|
||||
$this->drupalPostForm('node/add/moderated_content', [
|
||||
'title[0][value]' => 'moderated content',
|
||||
], t('Save and Create New Draft'));
|
||||
$nodes = \Drupal::entityTypeManager()
|
||||
->getStorage('node')
|
||||
->loadByProperties([
|
||||
'title' => 'moderated content',
|
||||
]);
|
||||
|
||||
if (!$nodes) {
|
||||
$this->fail('Test node was not saved correctly.');
|
||||
return;
|
||||
}
|
||||
|
||||
$node = reset($nodes);
|
||||
$this->assertEqual('draft', $node->moderation_state->target_id);
|
||||
|
||||
$path = 'node/' . $node->id() . '/edit';
|
||||
// Set up published revision.
|
||||
$this->drupalPostForm($path, [], t('Save and Publish'));
|
||||
\Drupal::entityTypeManager()->getStorage('node')->resetCache([$node->id()]);
|
||||
/* @var \Drupal\node\NodeInterface $node */
|
||||
$node = \Drupal::entityTypeManager()->getStorage('node')->load($node->id());
|
||||
$this->assertTrue($node->isPublished());
|
||||
$this->assertEqual('published', $node->moderation_state->target_id);
|
||||
|
||||
// Verify that the state field is not shown.
|
||||
$this->assertNoText('Published');
|
||||
|
||||
// Delete the node.
|
||||
$this->drupalPostForm('node/' . $node->id() . '/delete', array(), t('Delete'));
|
||||
$this->assertText(t('The Moderated content moderated content has been deleted.'));
|
||||
|
||||
$this->drupalGet('admin/structure/types/manage/moderated_content/moderation');
|
||||
$this->assertFieldByName('enable_moderation_state');
|
||||
$this->assertFieldChecked('edit-enable-moderation-state');
|
||||
$this->drupalPostForm(NULL, ['enable_moderation_state' => FALSE], t('Save'));
|
||||
$this->drupalGet('admin/structure/types/manage/moderated_content/moderation');
|
||||
$this->assertFieldByName('enable_moderation_state');
|
||||
$this->assertNoFieldChecked('edit-enable-moderation-state');
|
||||
$this->drupalPostForm('node/add/moderated_content', [
|
||||
'title[0][value]' => 'non-moderated content',
|
||||
], t('Save and publish'));
|
||||
|
||||
$nodes = \Drupal::entityTypeManager()
|
||||
->getStorage('node')
|
||||
->loadByProperties([
|
||||
'title' => 'non-moderated content',
|
||||
]);
|
||||
|
||||
if (!$nodes) {
|
||||
$this->fail('Non-moderated test node was not saved correctly.');
|
||||
return;
|
||||
}
|
||||
|
||||
$node = reset($nodes);
|
||||
$this->assertEqual(NULL, $node->moderation_state->target_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests edit form destinations.
|
||||
*/
|
||||
public function testFormSaveDestination() {
|
||||
// Create new moderated content in draft.
|
||||
$this->drupalPostForm('node/add/moderated_content', [
|
||||
'title[0][value]' => 'Some moderated content',
|
||||
'body[0][value]' => 'First version of the content.',
|
||||
], t('Save and Create New Draft'));
|
||||
|
||||
$node = $this->drupalGetNodeByTitle('Some moderated content');
|
||||
$edit_path = sprintf('node/%d/edit', $node->id());
|
||||
|
||||
// After saving, we should be at the canonical URL and viewing the first
|
||||
// revision.
|
||||
$this->assertUrl(Url::fromRoute('entity.node.canonical', ['node' => $node->id()]));
|
||||
$this->assertText('First version of the content.');
|
||||
|
||||
// Create a new draft; after saving, we should still be on the canonical
|
||||
// URL, but viewing the second revision.
|
||||
$this->drupalPostForm($edit_path, [
|
||||
'body[0][value]' => 'Second version of the content.',
|
||||
], t('Save and Create New Draft'));
|
||||
$this->assertUrl(Url::fromRoute('entity.node.canonical', ['node' => $node->id()]));
|
||||
$this->assertText('Second version of the content.');
|
||||
|
||||
// Make a new published revision; after saving, we should be at the
|
||||
// canonical URL.
|
||||
$this->drupalPostForm($edit_path, [
|
||||
'body[0][value]' => 'Third version of the content.',
|
||||
], t('Save and Publish'));
|
||||
$this->assertUrl(Url::fromRoute('entity.node.canonical', ['node' => $node->id()]));
|
||||
$this->assertText('Third version of the content.');
|
||||
|
||||
// Make a new forward revision; after saving, we should be on the "Latest
|
||||
// version" tab.
|
||||
$this->drupalPostForm($edit_path, [
|
||||
'body[0][value]' => 'Fourth version of the content.',
|
||||
], t('Save and Create New Draft'));
|
||||
$this->assertUrl(Url::fromRoute('entity.node.latest_version', ['node' => $node->id()]));
|
||||
$this->assertText('Fourth version of the content.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests pagers aren't broken by content_moderation.
|
||||
*/
|
||||
public function testPagers() {
|
||||
// Create 51 nodes to force the pager.
|
||||
foreach (range(1, 51) as $delta) {
|
||||
Node::create([
|
||||
'type' => 'moderated_content',
|
||||
'uid' => $this->adminUser->id(),
|
||||
'title' => 'Node ' . $delta,
|
||||
'status' => 1,
|
||||
'moderation_state' => 'published',
|
||||
])->save();
|
||||
}
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalGet('admin/content');
|
||||
$element = $this->cssSelect('nav.pager li.is-active a');
|
||||
$url = (string) $element[0]['href'];
|
||||
$query = [];
|
||||
parse_str(parse_url($url, PHP_URL_QUERY), $query);
|
||||
$this->assertEqual(0, $query['page']);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
|
||||
/**
|
||||
* Tests moderation state node type integration.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationStateNodeTypeTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* A node type without moderation state disabled.
|
||||
*/
|
||||
public function testNotModerated() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->createContentTypeFromUi('Not moderated', 'not_moderated');
|
||||
$this->assertText('The content type Not moderated has been added.');
|
||||
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'not_moderated');
|
||||
$this->drupalGet('node/add/not_moderated');
|
||||
$this->assertRaw('Save as unpublished');
|
||||
$this->drupalPostForm(NULL, [
|
||||
'title[0][value]' => 'Test',
|
||||
], t('Save and publish'));
|
||||
$this->assertText('Not moderated Test has been created.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests enabling moderation on an existing node-type, with content.
|
||||
*/
|
||||
public function testEnablingOnExistingContent() {
|
||||
// Create a node type that is not moderated.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->createContentTypeFromUi('Not moderated', 'not_moderated');
|
||||
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'not_moderated');
|
||||
|
||||
// Create content.
|
||||
$this->drupalGet('node/add/not_moderated');
|
||||
$this->drupalPostForm(NULL, [
|
||||
'title[0][value]' => 'Test',
|
||||
], t('Save and publish'));
|
||||
$this->assertText('Not moderated Test has been created.');
|
||||
|
||||
// Now enable moderation state.
|
||||
$this->enableModerationThroughUi(
|
||||
'not_moderated',
|
||||
['draft', 'needs_review', 'published'],
|
||||
'draft'
|
||||
);
|
||||
|
||||
// And make sure it works.
|
||||
$nodes = \Drupal::entityTypeManager()->getStorage('node')
|
||||
->loadByProperties(['title' => 'Test']);
|
||||
if (empty($nodes)) {
|
||||
$this->fail('Could not load node with title Test');
|
||||
return;
|
||||
}
|
||||
$node = reset($nodes);
|
||||
$this->drupalGet('node/' . $node->id());
|
||||
$this->assertResponse(200);
|
||||
$this->assertLinkByHref('node/' . $node->id() . '/edit');
|
||||
$this->drupalGet('node/' . $node->id() . '/edit');
|
||||
$this->assertResponse(200);
|
||||
$this->assertRaw('Save and Create New Draft');
|
||||
$this->assertNoRaw('Save and publish');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
/**
|
||||
* Tests moderation state config entity.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationStateStatesTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* Tests route access/permissions.
|
||||
*/
|
||||
public function testAccess() {
|
||||
$paths = [
|
||||
'admin/config/workflow/moderation',
|
||||
'admin/config/workflow/moderation/states',
|
||||
'admin/config/workflow/moderation/states/add',
|
||||
'admin/config/workflow/moderation/states/draft',
|
||||
'admin/config/workflow/moderation/states/draft/delete',
|
||||
];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
$this->drupalGet($path);
|
||||
// No access.
|
||||
$this->assertResponse(403);
|
||||
}
|
||||
$this->drupalLogin($this->adminUser);
|
||||
foreach ($paths as $path) {
|
||||
$this->drupalGet($path);
|
||||
// User has access.
|
||||
$this->assertResponse(200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests administration of moderation state entity.
|
||||
*/
|
||||
public function testStateAdministration() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalGet('admin/config/workflow/moderation');
|
||||
$this->assertLink('Moderation states');
|
||||
$this->assertLink('Moderation state transitions');
|
||||
$this->clickLink('Moderation states');
|
||||
$this->assertLink('Add moderation state');
|
||||
$this->assertText('Draft');
|
||||
// Edit the draft.
|
||||
$this->clickLink('Edit', 0);
|
||||
$this->assertFieldByName('label', 'Draft');
|
||||
$this->assertNoFieldChecked('edit-published');
|
||||
$this->drupalPostForm(NULL, [
|
||||
'label' => 'Drafty',
|
||||
], t('Save'));
|
||||
$this->assertText('Saved the Drafty Moderation state.');
|
||||
$this->drupalGet('admin/config/workflow/moderation/states/draft');
|
||||
$this->assertFieldByName('label', 'Drafty');
|
||||
$this->drupalPostForm(NULL, [
|
||||
'label' => 'Draft',
|
||||
], t('Save'));
|
||||
$this->assertText('Saved the Draft Moderation state.');
|
||||
$this->clickLink(t('Add moderation state'));
|
||||
$this->drupalPostForm(NULL, [
|
||||
'label' => 'Expired',
|
||||
'id' => 'expired',
|
||||
], t('Save'));
|
||||
$this->assertText('Created the Expired Moderation state.');
|
||||
$this->drupalGet('admin/config/workflow/moderation/states/expired');
|
||||
$this->clickLink('Delete');
|
||||
$this->assertText('Are you sure you want to delete Expired?');
|
||||
$this->drupalPostForm(NULL, [], t('Delete'));
|
||||
$this->assertText('Moderation state Expired deleted');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
use Drupal\user\Entity\Role;
|
||||
use Drupal\content_moderation\Entity\ModerationState;
|
||||
|
||||
/**
|
||||
* Defines a base class for moderation state tests.
|
||||
*/
|
||||
abstract class ModerationStateTestBase extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Profile to use.
|
||||
*/
|
||||
protected $profile = 'testing';
|
||||
|
||||
/**
|
||||
* Admin user.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'administer moderation states',
|
||||
'administer moderation state transitions',
|
||||
'use draft_draft transition',
|
||||
'use draft_published transition',
|
||||
'use published_draft transition',
|
||||
'use published_archived transition',
|
||||
'access administration pages',
|
||||
'administer content types',
|
||||
'administer nodes',
|
||||
'view latest version',
|
||||
'view any unpublished content',
|
||||
'access content overview',
|
||||
];
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation',
|
||||
'block',
|
||||
'block_content',
|
||||
'node',
|
||||
];
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalPlaceBlock('local_tasks_block', ['id' => 'tabs_block']);
|
||||
$this->drupalPlaceBlock('page_title_block');
|
||||
$this->drupalPlaceBlock('local_actions_block', ['id' => 'actions_block']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a content-type from the UI.
|
||||
*
|
||||
* @param string $content_type_name
|
||||
* Content type human name.
|
||||
* @param string $content_type_id
|
||||
* Machine name.
|
||||
* @param bool $moderated
|
||||
* TRUE if should be moderated.
|
||||
* @param string[] $allowed_states
|
||||
* Array of allowed state IDs.
|
||||
* @param string $default_state
|
||||
* Default state.
|
||||
*/
|
||||
protected function createContentTypeFromUi($content_type_name, $content_type_id, $moderated = FALSE, array $allowed_states = [], $default_state = NULL) {
|
||||
$this->drupalGet('admin/structure/types');
|
||||
$this->clickLink('Add content type');
|
||||
$edit = [
|
||||
'name' => $content_type_name,
|
||||
'type' => $content_type_id,
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save content type'));
|
||||
|
||||
if ($moderated) {
|
||||
$this->enableModerationThroughUi($content_type_id, $allowed_states, $default_state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable moderation for a specified content type, using the UI.
|
||||
*
|
||||
* @param string $content_type_id
|
||||
* Machine name.
|
||||
* @param string[] $allowed_states
|
||||
* Array of allowed state IDs.
|
||||
* @param string $default_state
|
||||
* Default state.
|
||||
*/
|
||||
protected function enableModerationThroughUi($content_type_id, array $allowed_states, $default_state) {
|
||||
$this->drupalGet('admin/structure/types');
|
||||
$this->assertLinkByHref('admin/structure/types/manage/' . $content_type_id . '/moderation');
|
||||
$this->drupalGet('admin/structure/types/manage/' . $content_type_id);
|
||||
$this->assertLinkByHref('admin/structure/types/manage/' . $content_type_id . '/moderation');
|
||||
$this->drupalGet('admin/structure/types/manage/' . $content_type_id . '/moderation');
|
||||
$this->assertFieldByName('enable_moderation_state');
|
||||
$this->assertNoFieldChecked('edit-enable-moderation-state');
|
||||
|
||||
$edit['enable_moderation_state'] = 1;
|
||||
|
||||
/** @var ModerationState $state */
|
||||
foreach (ModerationState::loadMultiple() as $state) {
|
||||
$key = $state->isPublishedState() ? 'allowed_moderation_states_published[' . $state->id() . ']' : 'allowed_moderation_states_unpublished[' . $state->id() . ']';
|
||||
$edit[$key] = in_array($state->id(), $allowed_states, TRUE) ? $state->id() : FALSE;
|
||||
}
|
||||
|
||||
$edit['default_moderation_state'] = $default_state;
|
||||
|
||||
$this->drupalPostForm(NULL, $edit, t('Save'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants given user permission to create content of given type.
|
||||
*
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* User to grant permission to.
|
||||
* @param string $content_type_id
|
||||
* Content type ID.
|
||||
*/
|
||||
protected function grantUserPermissionToCreateContentOfType(AccountInterface $account, $content_type_id) {
|
||||
$role_ids = $account->getRoles(TRUE);
|
||||
/* @var \Drupal\user\RoleInterface $role */
|
||||
$role_id = reset($role_ids);
|
||||
$role = Role::load($role_id);
|
||||
$role->grantPermission(sprintf('create %s content', $content_type_id));
|
||||
$role->grantPermission(sprintf('edit any %s content', $content_type_id));
|
||||
$role->grantPermission(sprintf('delete any %s content', $content_type_id));
|
||||
$role->save();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
/**
|
||||
* Tests moderation state transition config entity.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationStateTransitionsTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* Tests route access/permissions.
|
||||
*/
|
||||
public function testAccess() {
|
||||
$paths = [
|
||||
'admin/config/workflow/moderation/transitions',
|
||||
'admin/config/workflow/moderation/transitions/add',
|
||||
'admin/config/workflow/moderation/transitions/draft_published',
|
||||
'admin/config/workflow/moderation/transitions/draft_published/delete',
|
||||
];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
$this->drupalGet($path);
|
||||
// No access.
|
||||
$this->assertResponse(403);
|
||||
}
|
||||
$this->drupalLogin($this->adminUser);
|
||||
foreach ($paths as $path) {
|
||||
$this->drupalGet($path);
|
||||
// User has access.
|
||||
$this->assertResponse(200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests administration of moderation state transition entity.
|
||||
*/
|
||||
public function testTransitionAdministration() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
$this->drupalGet('admin/config/workflow/moderation');
|
||||
$this->clickLink('Moderation state transitions');
|
||||
$this->assertLink('Add moderation state transition');
|
||||
$this->assertText('Create New Draft');
|
||||
|
||||
// Edit the Draft » Draft review.
|
||||
$this->drupalGet('admin/config/workflow/moderation/transitions/draft_draft');
|
||||
$this->assertFieldByName('label', 'Create New Draft');
|
||||
$this->assertFieldByName('stateFrom', 'draft');
|
||||
$this->assertFieldByName('stateTo', 'draft');
|
||||
$this->drupalPostForm(NULL, [
|
||||
'label' => 'Create Draft',
|
||||
], t('Save'));
|
||||
$this->assertText('Saved the Create Draft Moderation state transition.');
|
||||
$this->drupalGet('admin/config/workflow/moderation/transitions/draft_draft');
|
||||
$this->assertFieldByName('label', 'Create Draft');
|
||||
// Now set it back.
|
||||
$this->drupalPostForm(NULL, [
|
||||
'label' => 'Create New Draft',
|
||||
], t('Save'));
|
||||
$this->assertText('Saved the Create New Draft Moderation state transition.');
|
||||
|
||||
// Add a new state.
|
||||
$this->drupalGet('admin/config/workflow/moderation/states/add');
|
||||
$this->drupalPostForm(NULL, [
|
||||
'label' => 'Expired',
|
||||
'id' => 'expired',
|
||||
], t('Save'));
|
||||
$this->assertText('Created the Expired Moderation state.');
|
||||
|
||||
// Add a new transition.
|
||||
$this->drupalGet('admin/config/workflow/moderation/transitions');
|
||||
$this->clickLink(t('Add moderation state transition'));
|
||||
$this->drupalPostForm(NULL, [
|
||||
'label' => 'Published » Expired',
|
||||
'id' => 'published_expired',
|
||||
'stateFrom' => 'published',
|
||||
'stateTo' => 'expired',
|
||||
], t('Save'));
|
||||
$this->assertText('Created the Published » Expired Moderation state transition.');
|
||||
|
||||
// Delete the new transition.
|
||||
$this->drupalGet('admin/config/workflow/moderation/transitions/published_expired');
|
||||
$this->clickLink('Delete');
|
||||
$this->assertText('Are you sure you want to delete Published » Expired?');
|
||||
$this->drupalPostForm(NULL, [], t('Delete'));
|
||||
$this->assertText('Moderation transition Published » Expired deleted');
|
||||
}
|
||||
|
||||
}
|
108
web/core/modules/content_moderation/src/Tests/NodeAccessTest.php
Normal file
108
web/core/modules/content_moderation/src/Tests/NodeAccessTest.php
Normal file
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
/**
|
||||
* Tests permission access control around nodes.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class NodeAccessTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->createContentTypeFromUi(
|
||||
'Moderated content',
|
||||
'moderated_content',
|
||||
TRUE,
|
||||
['draft', 'published'],
|
||||
'draft'
|
||||
);
|
||||
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a non-admin user can still access the appropriate pages.
|
||||
*/
|
||||
public function testPageAccess() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
// Create a node to test with.
|
||||
$this->drupalPostForm('node/add/moderated_content', [
|
||||
'title[0][value]' => 'moderated content',
|
||||
], t('Save and Create New Draft'));
|
||||
$nodes = \Drupal::entityTypeManager()
|
||||
->getStorage('node')
|
||||
->loadByProperties([
|
||||
'title' => 'moderated content',
|
||||
]);
|
||||
|
||||
if (!$nodes) {
|
||||
$this->fail('Test node was not saved correctly.');
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \Drupal\node\NodeInterface $node */
|
||||
$node = reset($nodes);
|
||||
|
||||
$view_path = 'node/' . $node->id();
|
||||
$edit_path = 'node/' . $node->id() . '/edit';
|
||||
$latest_path = 'node/' . $node->id() . '/latest';
|
||||
|
||||
// Publish the node.
|
||||
$this->drupalPostForm($edit_path, [], t('Save and Publish'));
|
||||
|
||||
// Ensure access works correctly for anonymous users.
|
||||
$this->drupalLogout();
|
||||
|
||||
$this->drupalGet($edit_path);
|
||||
$this->assertResponse(403);
|
||||
|
||||
$this->drupalGet($latest_path);
|
||||
$this->assertResponse(403);
|
||||
$this->drupalGet($view_path);
|
||||
$this->assertResponse(200);
|
||||
|
||||
// Create a forward revision for the 'Latest revision' tab.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalPostForm($edit_path, [
|
||||
'title[0][value]' => 'moderated content revised',
|
||||
], t('Save and Create New Draft'));
|
||||
|
||||
// Now make a new user and verify that the new user's access is correct.
|
||||
$user = $this->createUser([
|
||||
'use draft_draft transition',
|
||||
'use published_draft transition',
|
||||
'view latest version',
|
||||
'view any unpublished content',
|
||||
]);
|
||||
$this->drupalLogin($user);
|
||||
|
||||
$this->drupalGet($edit_path);
|
||||
$this->assertResponse(403);
|
||||
|
||||
$this->drupalGet($latest_path);
|
||||
$this->assertResponse(200);
|
||||
$this->drupalGet($view_path);
|
||||
$this->assertResponse(200);
|
||||
|
||||
// Now make another user, who should not be able to see forward revisions.
|
||||
$user = $this->createUser([
|
||||
'use published_draft transition',
|
||||
]);
|
||||
$this->drupalLogin($user);
|
||||
|
||||
$this->drupalGet($edit_path);
|
||||
$this->assertResponse(403);
|
||||
|
||||
$this->drupalGet($latest_path);
|
||||
$this->assertResponse(403);
|
||||
$this->drupalGet($view_path);
|
||||
$this->assertResponse(200);
|
||||
}
|
||||
|
||||
}
|
266
web/core/modules/content_moderation/src/ViewsData.php
Normal file
266
web/core/modules/content_moderation/src/ViewsData.php
Normal file
|
@ -0,0 +1,266 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Provides the content_moderation views integration.
|
||||
*/
|
||||
class ViewsData {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The moderation information.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInformation;
|
||||
|
||||
/**
|
||||
* Creates a new ViewsData instance.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
|
||||
* The moderation information.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_information) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->moderationInformation = $moderation_information;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the views data.
|
||||
*
|
||||
* @return array
|
||||
* The views data.
|
||||
*/
|
||||
public function getViewsData() {
|
||||
$data = [];
|
||||
|
||||
$data['content_revision_tracker']['table']['group'] = $this->t('Content moderation (tracker)');
|
||||
|
||||
$data['content_revision_tracker']['entity_type'] = [
|
||||
'title' => $this->t('Entity type'),
|
||||
'field' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
'filter' => [
|
||||
'id' => 'string',
|
||||
],
|
||||
'argument' => [
|
||||
'id' => 'string',
|
||||
],
|
||||
'sort' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
];
|
||||
|
||||
$data['content_revision_tracker']['entity_id'] = [
|
||||
'title' => $this->t('Entity ID'),
|
||||
'field' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
'filter' => [
|
||||
'id' => 'numeric',
|
||||
],
|
||||
'argument' => [
|
||||
'id' => 'numeric',
|
||||
],
|
||||
'sort' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
];
|
||||
|
||||
$data['content_revision_tracker']['langcode'] = [
|
||||
'title' => $this->t('Entity language'),
|
||||
'field' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
'filter' => [
|
||||
'id' => 'language',
|
||||
],
|
||||
'argument' => [
|
||||
'id' => 'language',
|
||||
],
|
||||
'sort' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
];
|
||||
|
||||
$data['content_revision_tracker']['revision_id'] = [
|
||||
'title' => $this->t('Latest revision ID'),
|
||||
'field' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
'filter' => [
|
||||
'id' => 'numeric',
|
||||
],
|
||||
'argument' => [
|
||||
'id' => 'numeric',
|
||||
],
|
||||
'sort' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
];
|
||||
|
||||
$entity_types_with_moderation = array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $type) {
|
||||
return $this->moderationInformation->canModerateEntitiesOfEntityType($type);
|
||||
});
|
||||
|
||||
// Add a join for each entity type to the content_revision_tracker table.
|
||||
foreach ($entity_types_with_moderation as $entity_type_id => $entity_type) {
|
||||
/** @var \Drupal\views\EntityViewsDataInterface $views_data */
|
||||
// We need the views_data handler in order to get the table name later.
|
||||
if ($this->entityTypeManager->hasHandler($entity_type_id, 'views_data') && $views_data = $this->entityTypeManager->getHandler($entity_type_id, 'views_data')) {
|
||||
// Add a join from the entity base table to the revision tracker table.
|
||||
$base_table = $views_data->getViewsTableForEntityType($entity_type);
|
||||
$data['content_revision_tracker']['table']['join'][$base_table] = [
|
||||
'left_field' => $entity_type->getKey('id'),
|
||||
'field' => 'entity_id',
|
||||
'extra' => [
|
||||
[
|
||||
'field' => 'entity_type',
|
||||
'value' => $entity_type_id,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Some entity types might not be translatable.
|
||||
if ($entity_type->hasKey('langcode')) {
|
||||
$data['content_revision_tracker']['table']['join'][$base_table]['extra'][] = [
|
||||
'field' => 'langcode',
|
||||
'left_field' => $entity_type->getKey('langcode'),
|
||||
'operation' => '=',
|
||||
];
|
||||
}
|
||||
|
||||
// Add a relationship between the revision tracker table to the latest
|
||||
// revision on the entity revision table.
|
||||
$data['content_revision_tracker']['latest_revision__' . $entity_type_id] = [
|
||||
'title' => $this->t('@label latest revision', ['@label' => $entity_type->getLabel()]),
|
||||
'group' => $this->t('@label revision', ['@label' => $entity_type->getLabel()]),
|
||||
'relationship' => [
|
||||
'id' => 'standard',
|
||||
'label' => $this->t('@label latest revision', ['@label' => $entity_type->getLabel()]),
|
||||
'base' => $this->getRevisionViewsTableForEntityType($entity_type),
|
||||
'base field' => $entity_type->getKey('revision'),
|
||||
'relationship field' => 'revision_id',
|
||||
'extra' => [
|
||||
[
|
||||
'left_field' => 'entity_type',
|
||||
'value' => $entity_type_id,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Some entity types might not be translatable.
|
||||
if ($entity_type->hasKey('langcode')) {
|
||||
$data['content_revision_tracker']['latest_revision__' . $entity_type_id]['relationship']['extra'][] = [
|
||||
'left_field' => 'langcode',
|
||||
'field' => $entity_type->getKey('langcode'),
|
||||
'operation' => '=',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Provides a relationship from moderated entity to its moderation state
|
||||
// entity.
|
||||
$content_moderation_state_entity_type = \Drupal::entityTypeManager()->getDefinition('content_moderation_state');
|
||||
$content_moderation_state_entity_base_table = $content_moderation_state_entity_type->getDataTable() ?: $content_moderation_state_entity_type->getBaseTable();
|
||||
$content_moderation_state_entity_revision_base_table = $content_moderation_state_entity_type->getRevisionDataTable() ?: $content_moderation_state_entity_type->getRevisionTable();
|
||||
foreach ($entity_types_with_moderation as $entity_type_id => $entity_type) {
|
||||
$table = $entity_type->getDataTable() ?: $entity_type->getBaseTable();
|
||||
|
||||
$data[$table]['moderation_state'] = [
|
||||
'title' => t('Moderation state'),
|
||||
'relationship' => [
|
||||
'id' => 'standard',
|
||||
'label' => $this->t('@label moderation state', ['@label' => $entity_type->getLabel()]),
|
||||
'base' => $content_moderation_state_entity_base_table,
|
||||
'base field' => 'content_entity_id',
|
||||
'relationship field' => $entity_type->getKey('id'),
|
||||
'join_extra' => [
|
||||
[
|
||||
'field' => 'content_entity_type_id',
|
||||
'value' => $entity_type_id,
|
||||
],
|
||||
[
|
||||
'field' => 'content_entity_revision_id',
|
||||
'left_field' => $entity_type->getKey('revision'),
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable();
|
||||
$data[$revision_table]['moderation_state'] = [
|
||||
'title' => t('Moderation state'),
|
||||
'relationship' => [
|
||||
'id' => 'standard',
|
||||
'label' => $this->t('@label moderation state', ['@label' => $entity_type->getLabel()]),
|
||||
'base' => $content_moderation_state_entity_revision_base_table,
|
||||
'base field' => 'content_entity_revision_id',
|
||||
'relationship field' => $entity_type->getKey('revision'),
|
||||
'join_extra' => [
|
||||
[
|
||||
'field' => 'content_entity_type_id',
|
||||
'value' => $entity_type_id,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters the table and field information from hook_views_data().
|
||||
*
|
||||
* @param array $data
|
||||
* An array of all information about Views tables and fields, collected from
|
||||
* hook_views_data(), passed by reference.
|
||||
*
|
||||
* @see hook_views_data()
|
||||
*/
|
||||
public function alterViewsData(array &$data) {
|
||||
$entity_types_with_moderation = array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $type) {
|
||||
return $this->moderationInformation->canModerateEntitiesOfEntityType($type);
|
||||
});
|
||||
foreach ($entity_types_with_moderation as $type) {
|
||||
$data[$type->getRevisionTable()]['latest_revision'] = [
|
||||
'title' => t('Is Latest Revision'),
|
||||
'help' => t('Restrict the view to only revisions that are the latest revision of their entity.'),
|
||||
'filter' => ['id' => 'latest_revision'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the table of an entity type to be used as revision table in views.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type.
|
||||
*
|
||||
* @return string
|
||||
* The revision base table.
|
||||
*/
|
||||
protected function getRevisionViewsTableForEntityType(EntityTypeInterface $entity_type) {
|
||||
return $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable();
|
||||
}
|
||||
|
||||
}
|
Reference in a new issue