Update core 8.3.0

This commit is contained in:
Rob Davies 2017-04-13 15:53:35 +01:00
parent da7a7918f8
commit cd7a898e66
6144 changed files with 132297 additions and 87747 deletions

View file

@ -0,0 +1,114 @@
<?php
namespace Drupal\content_moderation;
use Drupal\workflows\StateInterface;
/**
* A value object representing a workflow state for content moderation.
*/
class ContentModerationState implements StateInterface {
/**
* The vanilla state object from the Workflow module.
*
* @var \Drupal\workflows\StateInterface
*/
protected $state;
/**
* If entities should be published if in this state.
*
* @var bool
*/
protected $published;
/**
* If entities should be the default revision if in this state.
*
* @var bool
*/
protected $defaultRevision;
/**
* ContentModerationState constructor.
*
* Decorates state objects to add methods to determine if an entity should be
* published or made the default revision.
*
* @param \Drupal\workflows\StateInterface $state
* The vanilla state object from the Workflow module.
* @param bool $published
* (optional) TRUE if entities should be published if in this state, FALSE
* if not. Defaults to FALSE.
* @param bool $default_revision
* (optional) TRUE if entities should be the default revision if in this
* state, FALSE if not. Defaults to FALSE.
*/
public function __construct(StateInterface $state, $published = FALSE, $default_revision = FALSE) {
$this->state = $state;
$this->published = $published;
$this->defaultRevision = $default_revision;
}
/**
* Determines if entities should be published if in this state.
*
* @return bool
*/
public function isPublishedState() {
return $this->published;
}
/**
* Determines if entities should be the default revision if in this state.
*
* @return bool
*/
public function isDefaultRevisionState() {
return $this->defaultRevision;
}
/**
* {@inheritdoc}
*/
public function id() {
return $this->state->id();
}
/**
* {@inheritdoc}
*/
public function label() {
return $this->state->label();
}
/**
* {@inheritdoc}
*/
public function weight() {
return $this->state->weight();
}
/**
* {@inheritdoc}
*/
public function canTransitionTo($to_state_id) {
return $this->state->canTransitionTo($to_state_id);
}
/**
* {@inheritdoc}
*/
public function getTransitionTo($to_state_id) {
return $this->state->getTransitionTo($to_state_id);
}
/**
* {@inheritdoc}
*/
public function getTransitions() {
return $this->state->getTransitions();
}
}

View file

@ -19,9 +19,9 @@ class ContentModerationStateStorageSchema extends SqlContentEntityStorageSchema
// 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'),
);
$schema['content_moderation_state_field_data']['indexes'] += [
'content_moderation_state__lookup' => ['content_entity_type_id', 'content_entity_id', 'content_entity_revision_id'],
];
return $schema;
}

View file

@ -55,14 +55,20 @@ class ContentModerationState extends ContentEntityBase implements ContentModerat
->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')
$fields['workflow'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Workflow'))
->setDescription(t('The workflow the moderation state is in.'))
->setSetting('target_type', 'workflow')
->setRequired(TRUE)
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->addConstraint('ModerationState', []);
->setRevisionable(TRUE);
$fields['moderation_state'] = BaseFieldDefinition::create('string')
->setLabel(t('Moderation state'))
->setDescription(t('The moderation state of the referenced content.'))
->setRequired(TRUE)
->setTranslatable(TRUE)
->setRevisionable(TRUE);
$fields['content_entity_type_id'] = BaseFieldDefinition::create('string')
->setLabel(t('Content entity type ID'))
@ -142,7 +148,7 @@ class ContentModerationState extends ContentEntityBase implements ContentModerat
* An array of default values.
*/
public static function getCurrentUserId() {
return array(\Drupal::currentUser()->id());
return [\Drupal::currentUser()->id()];
}
/**
@ -155,7 +161,7 @@ class ContentModerationState extends ContentEntityBase implements ContentModerat
if ($related_entity instanceof TranslatableInterface) {
$related_entity = $related_entity->getTranslation($this->activeLangcode);
}
$related_entity->moderation_state->target_id = $this->moderation_state->target_id;
$related_entity->moderation_state = $this->moderation_state;
return $related_entity->save();
}

View file

@ -2,9 +2,9 @@
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\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
@ -33,31 +33,11 @@ class ModerationHandler implements ModerationHandlerInterface, EntityHandlerInte
// 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);
// Update publishing status if it can be updated and if it needs updating.
if (($entity instanceof EntityPublishedInterface) && $entity->isPublished() !== $published_state) {
$published_state ? $entity->setPublished() : $entity->setUnpublished();
}
// 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();
}
/**

View file

@ -2,7 +2,6 @@
namespace Drupal\content_moderation\Entity\Handler;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Form\FormStateInterface;
@ -27,21 +26,6 @@ interface ModerationHandlerInterface {
*/
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.
*

View file

@ -2,24 +2,40 @@
namespace Drupal\content_moderation\Entity\Handler;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\content_moderation\ModerationInformationInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Customizations for node entities.
*/
class NodeModerationHandler extends ModerationHandler {
/**
* The moderation information service.
*
* @var \Drupal\content_moderation\ModerationInformationInterface
*/
protected $moderationInfo;
/**
* NodeModerationHandler constructor.
*
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
* The moderation information service.
*/
public function __construct(ModerationInformationInterface $moderation_info) {
$this->moderationInfo = $moderation_info;
}
/**
* {@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);
}
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$container->get('content_moderation.moderation_information')
);
}
/**
@ -38,29 +54,11 @@ class NodeModerationHandler extends ModerationHandler {
/* @var \Drupal\node\Entity\NodeType $entity */
$entity = $form_state->getFormObject()->getEntity();
if ($entity->getThirdPartySetting('content_moderation', 'enabled', FALSE)) {
if ($this->moderationInfo->getWorkflowForEntity($entity)) {
// 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;
}
}

View file

@ -1,102 +0,0 @@
<?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;
}
}

View file

@ -1,114 +0,0 @@
<?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;
}
}

View file

@ -2,14 +2,16 @@
namespace Drupal\content_moderation;
use Drupal\content_moderation\Entity\ContentModerationState;
use Drupal\content_moderation\Entity\ContentModerationState as ContentModerationStateEntity;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\content_moderation\Form\EntityModerationForm;
use Drupal\workflows\WorkflowInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@ -45,6 +47,13 @@ class EntityOperations implements ContainerInjectionInterface {
*/
protected $tracker;
/**
* The entity bundle information service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInfo;
/**
* Constructs a new EntityOperations object.
*
@ -56,12 +65,15 @@ class EntityOperations implements ContainerInjectionInterface {
* The form builder.
* @param \Drupal\content_moderation\RevisionTrackerInterface $tracker
* The revision tracker.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
* The entity bundle information service.
*/
public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, RevisionTrackerInterface $tracker) {
public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, RevisionTrackerInterface $tracker, EntityTypeBundleInfoInterface $bundle_info) {
$this->moderationInfo = $moderation_info;
$this->entityTypeManager = $entity_type_manager;
$this->formBuilder = $form_builder;
$this->tracker = $tracker;
$this->bundleInfo = $bundle_info;
}
/**
@ -72,7 +84,8 @@ class EntityOperations implements ContainerInjectionInterface {
$container->get('content_moderation.moderation_information'),
$container->get('entity_type.manager'),
$container->get('form_builder'),
$container->get('content_moderation.revision_tracker')
$container->get('content_moderation.revision_tracker'),
$container->get('entity_type.bundle.info')
);
}
@ -86,20 +99,20 @@ class EntityOperations implements ContainerInjectionInterface {
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();
if ($entity->moderation_state->value) {
$workflow = $this->moderationInfo->getWorkflowForEntity($entity);
/** @var \Drupal\content_moderation\ContentModerationState $current_state */
$current_state = $workflow->getState($entity->moderation_state->value);
// 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);
|| $current_state->isDefaultRevisionState()
|| !$this->isDefaultRevisionPublished($entity, $workflow);
// Fire per-entity-type logic for handling the save process.
$this->entityTypeManager->getHandler($entity->getEntityTypeId(), 'moderation')->onPresave($entity, $update_default_revision, $published_state);
$this->entityTypeManager->getHandler($entity->getEntityTypeId(), 'moderation')->onPresave($entity, $update_default_revision, $current_state->isPublishedState());
}
}
@ -140,15 +153,14 @@ class EntityOperations implements ContainerInjectionInterface {
* The entity to update or create a moderation state for.
*/
protected function updateOrCreateFromEntity(EntityInterface $entity) {
$moderation_state = $entity->moderation_state->target_id;
$moderation_state = $entity->moderation_state->value;
$workflow = $this->moderationInfo->getWorkflowForEntity($entity);
/** @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');
$moderation_state = $workflow->getInitialState()->id();
}
// @todo what if $entity->moderation_state->target_id is null at this point?
// @todo what if $entity->moderation_state is null at this point?
$entity_type_id = $entity->getEntityTypeId();
$entity_id = $entity->id();
$entity_revision_id = $entity->getRevisionId();
@ -157,6 +169,7 @@ class EntityOperations implements ContainerInjectionInterface {
$entities = $storage->loadByProperties([
'content_entity_type_id' => $entity_type_id,
'content_entity_id' => $entity_id,
'workflow' => $workflow->id(),
]);
/** @var \Drupal\content_moderation\ContentModerationStateInterface $content_moderation_state */
@ -166,6 +179,7 @@ class EntityOperations implements ContainerInjectionInterface {
'content_entity_type_id' => $entity_type_id,
'content_entity_id' => $entity_id,
]);
$content_moderation_state->workflow->target_id = $workflow->id();
}
else {
// Create a new revision.
@ -186,7 +200,7 @@ class EntityOperations implements ContainerInjectionInterface {
// 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);
ContentModerationStateEntity::updateOrCreateFromEntity($content_moderation_state);
}
/**
@ -241,11 +255,13 @@ class EntityOperations implements ContainerInjectionInterface {
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity being saved.
* @param \Drupal\workflows\WorkflowInterface $workflow
* The workflow being applied to the entity.
*
* @return bool
* TRUE if the default revision is published. FALSE otherwise.
*/
protected function isDefaultRevisionPublished(EntityInterface $entity) {
protected function isDefaultRevisionPublished(EntityInterface $entity, WorkflowInterface $workflow) {
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
$default_revision = $storage->load($entity->id());
@ -260,7 +276,7 @@ class EntityOperations implements ContainerInjectionInterface {
$default_revision = $default_revision->getTranslation($entity->language()->getId());
}
return $default_revision && $default_revision->moderation_state->entity->isPublishedState();
return $default_revision && $workflow->getState($default_revision->moderation_state->value)->isPublishedState();
}
}

View file

@ -9,6 +9,7 @@ use Drupal\Core\Entity\ContentEntityFormInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\BaseFieldDefinition;
@ -49,6 +50,13 @@ class EntityTypeInfo implements ContainerInjectionInterface {
*/
protected $entityTypeManager;
/**
* The bundle information service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInfo;
/**
* The current user.
*
@ -77,11 +85,16 @@ class EntityTypeInfo implements ContainerInjectionInterface {
* The moderation information service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Entity type manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
* Bundle information service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* Current user.
*/
public function __construct(TranslationInterface $translation, ModerationInformationInterface $moderation_information, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user) {
public function __construct(TranslationInterface $translation, ModerationInformationInterface $moderation_information, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info, AccountInterface $current_user) {
$this->stringTranslation = $translation;
$this->moderationInfo = $moderation_information;
$this->entityTypeManager = $entity_type_manager;
$this->bundleInfo = $bundle_info;
$this->currentUser = $current_user;
}
@ -93,6 +106,7 @@ class EntityTypeInfo implements ContainerInjectionInterface {
$container->get('string_translation'),
$container->get('content_moderation.moderation_information'),
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info'),
$container->get('current_user')
);
}
@ -109,9 +123,16 @@ class EntityTypeInfo implements ContainerInjectionInterface {
* @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')]);
foreach ($entity_types as $entity_type_id => $entity_type) {
// The ContentModerationState entity type should never be moderated.
if ($entity_type->isRevisionable() && $entity_type_id != 'content_moderation_state') {
$entity_types[$entity_type_id] = $this->addModerationToEntityType($entity_type);
// Add additional moderation support to entity types whose bundles are
// managed by a config entity type.
if ($entity_type->getBundleEntityType()) {
$entity_types[$entity_type->getBundleEntityType()] = $this->addModerationToBundleEntityType($entity_types[$entity_type->getBundleEntityType()]);
}
}
}
}
@ -127,7 +148,7 @@ class EntityTypeInfo implements ContainerInjectionInterface {
* @return \Drupal\Core\Entity\ContentEntityTypeInterface
* The modified content entity definition.
*/
protected function addModerationToEntity(ContentEntityTypeInterface $type) {
protected function addModerationToEntityType(ContentEntityTypeInterface $type) {
if (!$type->hasHandlerClass('moderation')) {
$handler_class = !empty($this->moderationHandlers[$type->id()]) ? $this->moderationHandlers[$type->id()] : ModerationHandler::class;
$type->setHandlerClass('moderation', $handler_class);
@ -161,7 +182,7 @@ class EntityTypeInfo implements ContainerInjectionInterface {
* @return \Drupal\Core\Config\Entity\ConfigEntityTypeInterface
* The modified config entity definition.
*/
protected function addModerationToEntityType(ConfigEntityTypeInterface $type) {
protected function addModerationToBundleEntityType(ConfigEntityTypeInterface $type) {
if ($type->hasLinkTemplate('edit-form') && !$type->hasLinkTemplate('moderation-form')) {
$type->setLinkTemplate('moderation-form', $type->getLinkTemplate('edit-form') . '/moderation');
}
@ -196,7 +217,7 @@ class EntityTypeInfo implements ContainerInjectionInterface {
$operations = [];
$type = $entity->getEntityType();
$bundle_of = $type->getBundleOf();
if ($this->currentUser->hasPermission('administer moderation states') && $bundle_of &&
if ($this->currentUser->hasPermission('administer content moderation') && $bundle_of &&
$this->moderationInfo->canModerateEntitiesOfEntityType($this->entityTypeManager->getDefinition($bundle_of))
) {
$operations['manage-moderation'] = [
@ -262,16 +283,12 @@ class EntityTypeInfo implements ContainerInjectionInterface {
* - 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];
$entity_types = array_filter($this->entityTypeManager->getDefinitions(), [$this->moderationInfo, 'canModerateEntitiesOfEntityType']);
foreach ($entity_types as $type_name => $type) {
foreach ($this->bundleInfo->getBundleInfo($type_name) as $bundle_id => $bundle) {
if ($this->moderationInfo->shouldModerateEntitiesOfBundle($type, $bundle_id)) {
yield ['entity' => $type_name, 'bundle' => $bundle_id];
}
}
}
}
@ -291,15 +308,15 @@ class EntityTypeInfo implements ContainerInjectionInterface {
}
$fields = [];
$fields['moderation_state'] = BaseFieldDefinition::create('entity_reference')
->setLabel($this->t('Moderation state'))
->setDescription($this->t('The moderation state of this piece of content.'))
$fields['moderation_state'] = BaseFieldDefinition::create('string')
->setLabel(t('Moderation state'))
->setDescription(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',
'region' => 'hidden',
'weight' => -5,
])
->setDisplayOptions('form', [
@ -316,24 +333,6 @@ class EntityTypeInfo implements ContainerInjectionInterface {
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.
*
@ -388,21 +387,4 @@ class EntityTypeInfo implements ContainerInjectionInterface {
}
}
/**
* 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();
});
}
}

View file

@ -2,12 +2,11 @@
namespace Drupal\content_moderation\Form;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
use Drupal\content_moderation\Plugin\WorkflowType\ContentModeration;
use Drupal\workflows\WorkflowInterface;
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;
/**
@ -50,146 +49,107 @@ class BundleModerationConfigurationForm extends EntityForm {
* {@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),
/* @var \Drupal\Core\Config\Entity\ConfigEntityInterface $bundle */
$bundle = $this->getEntity();
$bundle_of_entity_type = $this->entityTypeManager->getDefinition($bundle->getEntityType()->getBundleOf());
/* @var \Drupal\workflows\WorkflowInterface[] $workflows */
$workflows = $this->entityTypeManager->getStorage('workflow')->loadMultiple();
$options = array_map(function (WorkflowInterface $workflow) {
return $workflow->label();
}, array_filter($workflows, function (WorkflowInterface $workflow) {
return $workflow->status() && $workflow->getTypePlugin() instanceof ContentModeration;
}));
$selected_workflow = array_reduce($workflows, function ($carry, WorkflowInterface $workflow) use ($bundle_of_entity_type, $bundle) {
$plugin = $workflow->getTypePlugin();
if ($plugin instanceof ContentModeration && $plugin->appliesToEntityTypeAndBundle($bundle_of_entity_type->id(), $bundle->id())) {
return $workflow->id();
}
return $carry;
});
$form['workflow'] = [
'#type' => 'select',
'#title' => $this->t('Select the workflow to apply'),
'#default_value' => $selected_workflow,
'#options' => $options,
'#required' => FALSE,
'#empty_value' => '',
];
$form['original_workflow'] = [
'#type' => 'value',
'#value' => $selected_workflow,
];
$form['bundle'] = [
'#type' => 'value',
'#value' => $bundle->id(),
];
$form['entity_type'] = [
'#type' => 'value',
'#value' => $bundle_of_entity_type->id(),
];
// Add a special message when moderation is being disabled.
if ($bundle->getThirdPartySetting('content_moderation', 'enabled', FALSE)) {
$form['enable_moderation_state_note'] = [
if ($selected_workflow) {
$form['enable_workflow_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],
],
],
'#access' => !empty($selected_workflow)
];
}
$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.'));
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$entity_type_id = $form_state->getValue('entity_type');
$bundle_id = $form_state->getValue('bundle');
$new_workflow_id = $form_state->getValue('workflow');
$original_workflow_id = $form_state->getValue('original_workflow');
if ($new_workflow_id === $original_workflow_id) {
// Nothing to do.
return;
}
if ($original_workflow_id) {
/* @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->entityTypeManager->getStorage('workflow')->load($original_workflow_id);
$workflow->getTypePlugin()->removeEntityTypeAndBundle($entity_type_id, $bundle_id);
$workflow->save();
}
if ($new_workflow_id) {
/* @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->entityTypeManager->getStorage('workflow')->load($new_workflow_id);
$workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle_id);
$workflow->save();
}
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#submit' => ['::submitForm', '::save'],
];
return $actions;
}
}

View file

@ -3,12 +3,11 @@
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 Drupal\workflows\Transition;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@ -30,13 +29,6 @@ class EntityModerationForm extends FormBase {
*/
protected $validation;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* EntityModerationForm constructor.
*
@ -44,13 +36,10 @@ class EntityModerationForm extends FormBase {
* 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) {
public function __construct(ModerationInformationInterface $moderation_info, StateTransitionValidation $validation) {
$this->moderationInfo = $moderation_info;
$this->validation = $validation;
$this->entityTypeManager = $entity_type_manager;
}
/**
@ -59,8 +48,7 @@ class EntityModerationForm extends FormBase {
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')
$container->get('content_moderation.state_transition_validation')
);
}
@ -75,20 +63,21 @@ class EntityModerationForm extends FormBase {
* {@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;
$current_state = $entity->moderation_state->value;
$workflow = $this->moderationInfo->getWorkflowForEntity($entity);
/** @var \Drupal\workflows\Transition[] $transitions */
$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();
$transitions = array_filter($transitions, function(Transition $transition) use ($current_state) {
return $transition->to()->id() != $current_state;
});
$target_states = [];
/** @var ModerationStateTransition $transition */
foreach ($transitions as $transition) {
$target_states[$transition->getToState()] = $transition->label();
$target_states[$transition->to()->id()] = $transition->to()->label();
}
if (!count($target_states)) {
@ -99,7 +88,7 @@ class EntityModerationForm extends FormBase {
$form['current'] = [
'#type' => 'item',
'#title' => $this->t('Status'),
'#markup' => $current_state->label(),
'#markup' => $workflow->getState($current_state)->label(),
];
}
@ -137,23 +126,19 @@ class EntityModerationForm extends FormBase {
$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->set('moderation_state', $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);
$new_state = $this->moderationInfo->getWorkflowForEntity($entity)->getState($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()) {
if ($new_state->isDefaultRevisionState()) {
$form_state->setRedirectUrl($entity->toUrl('canonical'));
}
}

View file

@ -1,49 +0,0 @@
<?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());
}
}

View file

@ -1,82 +0,0 @@
<?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'));
}
}

View file

@ -1,49 +0,0 @@
<?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());
}
}

View file

@ -1,151 +0,0 @@
<?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'));
}
}

View file

@ -4,6 +4,7 @@ namespace Drupal\content_moderation;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
@ -19,16 +20,24 @@ class ModerationInformation implements ModerationInformationInterface {
*/
protected $entityTypeManager;
/**
* The bundle information service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInfo;
/**
* 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.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
* The bundle information service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info) {
$this->entityTypeManager = $entity_type_manager;
$this->bundleInfo = $bundle_info;
}
/**
@ -54,10 +63,8 @@ class ModerationInformation implements ModerationInformationInterface {
*/
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);
}
$bundles = $this->bundleInfo->getBundleInfo($entity_type->id());
return isset($bundles[$bundle]['workflow']);
}
return FALSE;
}
@ -123,10 +130,22 @@ class ModerationInformation implements ModerationInformationInterface {
* {@inheritdoc}
*/
public function isLiveRevision(ContentEntityInterface $entity) {
$workflow = $this->getWorkflowForEntity($entity);
return $this->isLatestRevision($entity)
&& $entity->isDefaultRevision()
&& $entity->moderation_state->entity
&& $entity->moderation_state->entity->isPublishedState();
&& $entity->moderation_state->value
&& $workflow->getState($entity->moderation_state->value)->isPublishedState();
}
/**
* {@inheritdoc}
*/
public function getWorkflowForEntity(ContentEntityInterface $entity) {
$bundles = $this->bundleInfo->getBundleInfo($entity->getEntityTypeId());
if (isset($bundles[$entity->bundle()]['workflow'])) {
return $this->entityTypeManager->getStorage('workflow')->load($bundles[$entity->bundle()]['workflow']);
};
return NULL;
}
}

View file

@ -126,4 +126,15 @@ interface ModerationInformationInterface {
*/
public function isLiveRevision(ContentEntityInterface $entity);
/**
* Gets the workflow for the given content entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The content entity to get the workflow for.
*
* @return \Drupal\workflows\WorkflowInterface|null
* The workflow entity. NULL if there is no workflow.
*/
public function getWorkflowForEntity(ContentEntityInterface $entity);
}

View file

@ -1,31 +0,0 @@
<?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;
}
}

View file

@ -1,28 +0,0 @@
<?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();
}

View file

@ -1,40 +0,0 @@
<?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);
}
}

View file

@ -1,36 +0,0 @@
<?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();
}

View file

@ -1,173 +0,0 @@
<?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;
}
}

View file

@ -95,7 +95,7 @@ class EntityRevisionConverter extends EntityConverter {
// 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'));
$latest_revision = $this->entityManager->getTranslationFromContext($latest_revision, NULL, ['operation' => 'entity_upcast']);
}
if ($latest_revision->isRevisionTranslationAffected()) {

View file

@ -3,8 +3,7 @@
namespace Drupal\content_moderation;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\content_moderation\Entity\ModerationState;
use Drupal\content_moderation\Entity\ModerationStateTransition;
use Drupal\workflows\Entity\Workflow;
/**
* Defines a class for dynamic permissions based on transitions.
@ -20,24 +19,20 @@ class Permissions {
* 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(),
]),
];
$permissions = [];
/** @var \Drupal\workflows\WorkflowInterface $workflow */
foreach (Workflow::loadMultipleByType('content_moderation') as $id => $workflow) {
foreach ($workflow->getTransitions() as $transition) {
$permissions['use ' . $workflow->id() . ' transition ' . $transition->id()] = [
'title' => $this->t('Use %transition transition from %workflow workflow.', [
'%transition' => $transition->label(),
'%workflow' => $workflow->label(),
]),
];
}
}
return $perms;
return $permissions;
}
}

View file

@ -53,23 +53,14 @@ class ModerationOptOutPublishNode extends PublishNode implements ContainerFactor
/**
* {@inheritdoc}
*/
public function execute($entity = NULL) {
public function access($entity, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $entity */
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;
drupal_set_message($this->t("@bundle @label were skipped as they are under moderation and may not be directly published.", ['@bundle' => node_get_type_label($entity), '@label' => $entity->getEntityType()->getPluralLabel()]), 'warning');
$result = AccessResult::forbidden();
return $return_as_object ? $result : $result->isAllowed();
}
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();
return parent::access($entity, $account, $return_as_object);
}
}

View file

@ -53,23 +53,14 @@ class ModerationOptOutUnpublishNode extends UnpublishNode implements ContainerFa
/**
* {@inheritdoc}
*/
public function execute($entity = NULL) {
public function access($entity, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $entity */
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;
drupal_set_message($this->t("@bundle @label were skipped as they are under moderation and may not be directly unpublished.", ['@bundle' => node_get_type_label($entity), '@label' => $entity->getEntityType()->getPluralLabel()]), 'warning');
$result = AccessResult::forbidden();
return $return_as_object ? $result : $result->isAllowed();
}
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();
return parent::access($entity, $account, $return_as_object);
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace Drupal\content_moderation\Plugin\Field\FieldFormatter;
use Drupal\content_moderation\ModerationInformationInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Plugin implementation of the 'content_moderation_state' formatter.
*
* @FieldFormatter(
* id = "content_moderation_state",
* label = @Translation("Content moderation state"),
* field_types = {
* "string",
* }
* )
*/
class ContentModerationStateFormatter extends FormatterBase implements ContainerFactoryPluginInterface {
/**
* The moderation information service.
*
* @var \Drupal\content_moderation\ModerationInformationInterface
*/
protected $moderationInformation;
/**
* Create an instance of ContentModerationStateFormatter.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, ModerationInformationInterface $moderation_information) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
$this->moderationInformation = $moderation_information;
}
/**
* {@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['label'],
$configuration['view_mode'],
$configuration['third_party_settings'],
$container->get('content_moderation.moderation_information')
);
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
$workflow = $this->moderationInformation->getWorkflowForEntity($items->getEntity());
foreach ($items as $delta => $item) {
$elements[$delta] = [
'#markup' => $workflow->getState($item->value)->label(),
];
}
return $elements;
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return $field_definition->getName() === 'moderation_state' && $field_definition->getTargetEntityTypeId() !== 'content_moderation_state';
}
}

View file

@ -3,9 +3,7 @@
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;
@ -23,7 +21,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* id = "moderation_state_default",
* label = @Translation("Moderation state"),
* field_types = {
* "entity_reference"
* "string"
* }
* )
*/
@ -36,20 +34,6 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
*/
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.
*
@ -64,13 +48,6 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
*/
protected $entityTypeManager;
/**
* Moderation state transition storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $moderationStateTransitionStorage;
/**
* Moderation state transition validation service.
*
@ -95,22 +72,13 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
* 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) {
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, 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;
@ -129,9 +97,6 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
$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')
);
@ -151,19 +116,18 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
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) {
$workflow = $this->moderationInformation->getWorkflowForEntity($entity);
$default = $items->get($delta)->value ? $workflow->getState($items->get($delta)->value) : $workflow->getInitialState();
if (!$default) {
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()));
}
/** @var \Drupal\workflows\Transition[] $transitions */
$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();
$target_states[$transition->to()->id()] = $transition->label();
}
// @todo https://www.drupal.org/node/2779933 write a test for this.
@ -171,11 +135,11 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
'#access' => FALSE,
'#type' => 'select',
'#options' => $target_states,
'#default_value' => $default,
'#published' => $default ? $default_state->isPublishedState() : FALSE,
'#default_value' => $default->id(),
'#published' => $default->isPublishedState(),
'#key_column' => $this->column,
];
$element['#element_validate'][] = array(get_class($this), 'validateElement');
$element['#element_validate'][] = [get_class($this), 'validateElement'];
// Use the dropbutton.
$element['#process'][] = [get_called_class(), 'processActions'];
@ -197,7 +161,7 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
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'];
$entity->moderation_state->value = $element['#moderation_state'];
}
}
@ -249,7 +213,7 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return parent::isApplicable($field_definition) && $field_definition->getName() === 'moderation_state';
return $field_definition->getName() === 'moderation_state';
}
}

View file

@ -2,8 +2,8 @@
namespace Drupal\content_moderation\Plugin\Field;
use Drupal\content_moderation\Entity\ModerationState;
use Drupal\Core\Field\EntityReferenceFieldItemList;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Field\FieldItemList;
/**
* A computed field that provides a content entity's moderation state.
@ -11,60 +11,75 @@ use Drupal\Core\Field\EntityReferenceFieldItemList;
* It links content entities to a moderation state configuration entity via a
* moderation state content entity.
*/
class ModerationStateFieldItemList extends EntityReferenceFieldItemList {
class ModerationStateFieldItemList extends FieldItemList {
/**
* Gets the moderation state entity linked to a content entity revision.
* Gets the moderation state ID linked to a content entity revision.
*
* @return \Drupal\content_moderation\ModerationStateInterface|null
* The moderation state configuration entity linked to a content entity
* revision.
* @return string|null
* The moderation state ID linked to a content entity revision.
*/
protected function getModerationState() {
protected function getModerationStateId() {
$entity = $this->getEntity();
if (!\Drupal::service('content_moderation.moderation_information')->shouldModerateEntitiesOfBundle($entity->getEntityType(), $entity->bundle())) {
/** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
$moderation_info = \Drupal::service('content_moderation.moderation_information');
if (!$moderation_info->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;
}
// Existing entities will have a corresponding content_moderation_state
// entity associated with them.
if (!$entity->isNew() && $content_moderation_state = $this->loadContentModerationStateRevision($entity)) {
return $content_moderation_state->moderation_state->value;
}
// 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);
$workflow = $moderation_info->getWorkflowForEntity($entity);
return $workflow ? $workflow->getInitialState()->id() : NULL;
}
/**
* Load the content moderation state revision associated with an entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity the content moderation state entity will be loaded from.
*
* @return \Drupal\content_moderation\ContentModerationStateInterface|null
* The content_moderation_state revision or FALSE if none exists.
*/
protected function loadContentModerationStateRevision(ContentEntityInterface $entity) {
$moderation_info = \Drupal::service('content_moderation.moderation_information');
$content_moderation_storage = \Drupal::entityTypeManager()->getStorage('content_moderation_state');
$revisions = \Drupal::service('entity.query')->get('content_moderation_state')
->condition('content_entity_type_id', $entity->getEntityTypeId())
->condition('content_entity_id', $entity->id())
// Ensure the correct revision is loaded in scenarios where a revision is
// being reverted.
->condition('content_entity_revision_id', $entity->isNewRevision() ? $entity->getLoadedRevisionId() : $entity->getRevisionId())
->condition('workflow', $moderation_info->getWorkflowForEntity($entity)->id())
->allRevisions()
->sort('revision_id', 'DESC')
->execute();
if (empty($revisions)) {
return NULL;
}
/** @var \Drupal\content_moderation\ContentModerationStateInterface $content_moderation_state */
$content_moderation_state = $content_moderation_storage->loadRevision(key($revisions));
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;
}
/**
@ -93,10 +108,11 @@ class ModerationStateFieldItemList extends EntityReferenceFieldItemList {
// Compute the value of the moderation state.
$index = 0;
if (!isset($this->list[$index]) || $this->list[$index]->isEmpty()) {
$moderation_state = $this->getModerationState();
$moderation_state = $this->getModerationStateId();
// Do not store NULL values in the static cache.
if ($moderation_state) {
$this->list[$index] = $this->createItem($index, ['entity' => $moderation_state]);
$this->list[$index] = $this->createItem($index, $moderation_state);
}
}
}

View file

@ -2,6 +2,7 @@
namespace Drupal\content_moderation\Plugin\Menu;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Menu\LocalTaskDefault;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\RouteMatchInterface;
@ -25,9 +26,9 @@ class EditTab extends LocalTaskDefault implements ContainerFactoryPluginInterfac
protected $moderationInfo;
/**
* The entity.
* The entity if determinable from the route or FALSE.
*
* @var \Drupal\Core\Entity\ContentEntityInterface
* @var \Drupal\Core\Entity\ContentEntityInterface|FALSE
*/
protected $entity;
@ -69,8 +70,8 @@ class EditTab extends LocalTaskDefault implements ContainerFactoryPluginInterfac
* {@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']);
$entity_parameter = $route_match->getParameter($this->pluginDefinition['entity_type_id']);
$this->entity = $entity_parameter instanceof ContentEntityInterface ? $route_match->getParameter($this->pluginDefinition['entity_type_id']) : FALSE;
return parent::getRouteParameters($route_match);
}
@ -78,12 +79,11 @@ class EditTab extends LocalTaskDefault implements ContainerFactoryPluginInterfac
* {@inheritdoc}
*/
public function getTitle() {
if (!$this->moderationInfo->isModeratedEntity($this->entity)) {
// Moderation isn't enabled.
// If the entity couldn't be loaded or moderation isn't enabled.
if (!$this->entity || !$this->moderationInfo->isModeratedEntity($this->entity)) {
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');
@ -93,11 +93,12 @@ class EditTab extends LocalTaskDefault implements ContainerFactoryPluginInterfac
* {@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();
if ($this->entity) {
$tags = array_merge($tags, $this->entity->getCacheTags());
$tags[] = $this->entity->getEntityType()->getBundleEntityType() . ':' . $this->entity->bundle();
}
return $tags;
}

View file

@ -2,7 +2,6 @@
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;
@ -93,21 +92,13 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
$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
$workflow = $this->moderationInformation->getWorkflowForEntity($entity);
$new_state = $workflow->getState($entity->moderation_state->value) ?: $workflow->getInitialState();
$original_state = $workflow->getState($original_entity->moderation_state->value);
// @todo - what if $new_state 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()]);
if (!$original_state->canTransitionTo($new_state->id())) {
$this->context->addViolation($constraint->message, ['%from' => $original_state->label(), '%to' => $new_state->label()]);
}
}
@ -126,9 +117,9 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
protected function isFirstTimeModeration(EntityInterface $entity) {
$original_entity = $this->moderationInformation->getLatestRevision($entity->getEntityTypeId(), $entity->id());
$original_id = $original_entity->moderation_state->target_id;
$original_id = $original_entity->moderation_state;
return !($entity->moderation_state->target_id && $original_entity && $original_id);
return !($entity->moderation_state && $original_entity && $original_id);
}
}

View file

@ -0,0 +1,292 @@
<?php
namespace Drupal\content_moderation\Plugin\WorkflowType;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\content_moderation\ContentModerationState;
use Drupal\workflows\Plugin\WorkflowTypeBase;
use Drupal\workflows\StateInterface;
use Drupal\workflows\WorkflowInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Attaches workflows to content entity types and their bundles.
*
* @WorkflowType(
* id = "content_moderation",
* label = @Translation("Content moderation"),
* required_states = {
* "draft",
* "published",
* },
* )
*/
class ContentModeration extends WorkflowTypeBase implements ContainerFactoryPluginInterface {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Creates an instance of the ContentModeration WorkflowType plugin.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@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')
);
}
/**
* {@inheritdoc}
*/
public function initializeWorkflow(WorkflowInterface $workflow) {
$workflow
->addState('draft', $this->t('Draft'))
->setStateWeight('draft', -5)
->addState('published', $this->t('Published'))
->setStateWeight('published', 0)
->addTransition('create_new_draft', $this->t('Create New Draft'), ['draft', 'published'], 'draft')
->addTransition('publish', $this->t('Publish'), ['draft', 'published'], 'published');
return $workflow;
}
/**
* {@inheritdoc}
*/
public function checkWorkflowAccess(WorkflowInterface $entity, $operation, AccountInterface $account) {
if ($operation === 'view') {
return AccessResult::allowedIfHasPermission($account, 'view content moderation');
}
return parent::checkWorkflowAccess($entity, $operation, $account);
}
/**
* {@inheritdoc}
*/
public function decorateState(StateInterface $state) {
if (isset($this->configuration['states'][$state->id()])) {
$state = new ContentModerationState($state, $this->configuration['states'][$state->id()]['published'], $this->configuration['states'][$state->id()]['default_revision']);
}
else {
$state = new ContentModerationState($state);
}
return $state;
}
/**
* {@inheritdoc}
*/
public function buildStateConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, StateInterface $state = NULL) {
/** @var \Drupal\content_moderation\ContentModerationState $state */
$is_required_state = isset($state) ? in_array($state->id(), $this->getRequiredStates(), TRUE) : FALSE;
$form = [];
$form['published'] = [
'#type' => 'checkbox',
'#title' => $this->t('Published'),
'#description' => $this->t('When content reaches this state it should be published.'),
'#default_value' => isset($state) ? $state->isPublishedState() : FALSE,
'#disabled' => $is_required_state,
];
$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' => isset($state) ? $state->isDefaultRevisionState() : FALSE,
'#disabled' => $is_required_state,
// @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;
}
/**
* Gets the entity types the workflow is applied to.
*
* @return string[]
* The entity types the workflow is applied to.
*/
public function getEntityTypes() {
return array_keys($this->configuration['entity_types']);
}
/**
* Gets any bundles the workflow is applied to for the given entity type.
*
* @param string $entity_type_id
* The entity type ID to get the bundles for.
*
* @return string[]
* The bundles of the entity type the workflow is applied to or an empty
* array if the entity type is not applied to the workflow.
*/
public function getBundlesForEntityType($entity_type_id) {
return isset($this->configuration['entity_types'][$entity_type_id]) ? $this->configuration['entity_types'][$entity_type_id] : [];
}
/**
* Checks if the workflow applies to the supplied entity type and bundle.
*
* @param string $entity_type_id
* The entity type ID to check.
* @param string $bundle_id
* The bundle ID to check.
*
* @return bool
* TRUE if the workflow applies to the supplied entity type ID and bundle
* ID. FALSE if not.
*/
public function appliesToEntityTypeAndBundle($entity_type_id, $bundle_id) {
return in_array($bundle_id, $this->getBundlesForEntityType($entity_type_id), TRUE);
}
/**
* Removes an entity type ID / bundle ID from the workflow.
*
* @param string $entity_type_id
* The entity type ID to remove.
* @param string $bundle_id
* The bundle ID to remove.
*/
public function removeEntityTypeAndBundle($entity_type_id, $bundle_id) {
$key = array_search($bundle_id, $this->configuration['entity_types'][$entity_type_id], TRUE);
if ($key !== FALSE) {
unset($this->configuration['entity_types'][$entity_type_id][$key]);
if (empty($this->configuration['entity_types'][$entity_type_id])) {
unset($this->configuration['entity_types'][$entity_type_id]);
}
else {
$this->configuration['entity_types'][$entity_type_id] = array_values($this->configuration['entity_types'][$entity_type_id]);
}
}
}
/**
* Add an entity type ID / bundle ID to the workflow.
*
* @param string $entity_type_id
* The entity type ID to add. It is responsibility of the caller to provide
* a valid entity type ID.
* @param string $bundle_id
* The bundle ID to add. It is responsibility of the caller to provide a
* valid bundle ID.
*/
public function addEntityTypeAndBundle($entity_type_id, $bundle_id) {
if (!$this->appliesToEntityTypeAndBundle($entity_type_id, $bundle_id)) {
$this->configuration['entity_types'][$entity_type_id][] = $bundle_id;
sort($this->configuration['entity_types'][$entity_type_id]);
ksort($this->configuration['entity_types']);
}
}
/**
* {@inheritDoc}
*/
public function defaultConfiguration() {
// This plugin does not store anything per transition.
return [
'states' => [
'draft' => [
'published' => FALSE,
'default_revision' => FALSE,
],
'published' => [
'published' => TRUE,
'default_revision' => TRUE,
],
],
'entity_types' => [],
];
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
foreach ($this->getEntityTypes() as $entity_type_id) {
$entity_definition = $this->entityTypeManager->getDefinition($entity_type_id);
foreach ($this->getBundlesForEntityType($entity_type_id) as $bundle) {
$dependency = $entity_definition->getBundleConfigDependency($bundle);
$dependencies[$dependency['type']][] = $dependency['name'];
}
}
return $dependencies;
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
$changed = parent::onDependencyRemoval($dependencies);
// When bundle config entities are removed, ensure they are cleaned up from
// the workflow.
foreach ($dependencies['config'] as $removed_config) {
if ($entity_type_id = $removed_config->getEntityType()->getBundleOf()) {
$bundle_id = $removed_config->id();
$this->removeEntityTypeAndBundle($entity_type_id, $bundle_id);
$changed = TRUE;
}
}
// When modules that provide entity types are removed, ensure they are also
// removed from the workflow.
if (!empty($dependencies['module'])) {
// Gather all entity definitions provided by the dependent modules which
// are being removed.
$module_entity_definitions = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_definition) {
if (in_array($entity_definition->getProvider(), $dependencies['module'])) {
$module_entity_definitions[] = $entity_definition;
}
}
// For all entity types provided by the uninstalled modules, remove any
// configuration for those types.
foreach ($module_entity_definitions as $module_entity_definition) {
foreach ($this->getBundlesForEntityType($module_entity_definition->id()) as $bundle) {
$this->removeEntityTypeAndBundle($module_entity_definition->id(), $bundle);
$changed = TRUE;
}
}
}
return $changed;
}
/**
* {@inheritdoc}
*/
public function getConfiguration() {
$configuration = parent::getConfiguration();
// Ensure that states and entity types are ordered consistently.
ksort($configuration['states']);
ksort($configuration['entity_types']);
return $configuration;
}
}

View file

@ -111,7 +111,7 @@ class EntityModerationRouteProvider implements EntityRouteProviderInterface, Ent
* type does not support fields.
*/
protected function getEntityTypeIdKeyType(EntityTypeInterface $entity_type) {
if (!$entity_type->isSubclassOf(FieldableEntityInterface::class)) {
if (!$entity_type->entityClassImplements(FieldableEntityInterface::class)) {
return NULL;
}

View file

@ -47,7 +47,7 @@ class EntityTypeModerationRouteProvider implements EntityRouteProviderInterface
'_entity_form' => "{$entity_type_id}.moderation",
'_title' => 'Moderation',
])
->setRequirement('_permission', 'administer moderation states')
->setRequirement('_permission', 'administer content moderation')
->setOption('parameters', [
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
]);

View file

@ -3,10 +3,8 @@
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;
use Drupal\workflows\Transition;
/**
* Validates whether a certain state transition is allowed.
@ -14,18 +12,11 @@ use Drupal\content_moderation\Entity\ModerationStateTransition;
class StateTransitionValidation implements StateTransitionValidationInterface {
/**
* Entity type manager.
* The moderation information service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
* @var \Drupal\content_moderation\ModerationInformationInterface
*/
protected $entityTypeManager;
/**
* Entity query factory.
*
* @var \Drupal\Core\Entity\Query\QueryFactory
*/
protected $queryFactory;
protected $moderationInfo;
/**
* Stores the possible state transitions.
@ -37,211 +28,23 @@ class StateTransitionValidation implements StateTransitionValidationInterface {
/**
* 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.
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
* The moderation information service.
*/
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);
});
public function __construct(ModerationInformationInterface $moderation_info) {
$this->moderationInfo = $moderation_info;
}
/**
* {@inheritdoc}
*/
public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user) {
$bundle = $this->loadBundleEntity($entity->getEntityType()->getBundleEntityType(), $entity->bundle());
$workflow = $this->moderationInfo->getWorkflowForEntity($entity);
$current_state = $entity->moderation_state->value ? $workflow->getState($entity->moderation_state->value) : $workflow->getInitialState();
/** @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 array_filter($current_state->getTransitions(), function(Transition $transition) use ($workflow, $user) {
return $user->hasPermission('use ' . $workflow->id() . ' transition ' . $transition->id());
});
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);
}
}

View file

@ -10,20 +10,6 @@ use Drupal\Core\Session\AccountInterface;
*/
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.
*
@ -32,40 +18,9 @@ interface StateTransitionValidationInterface {
* @param \Drupal\Core\Session\AccountInterface $user
* The account that wants to perform a transition.
*
* @return \Drupal\content_moderation\Entity\ModerationStateTransition[]
* @return \Drupal\workflows\Transition[]
* 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);
}

View file

@ -15,10 +15,7 @@ class ModerationFormTest extends ModerationStateTestBase {
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->adminUser);
$this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE, [
'draft',
'published',
], 'draft');
$this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE);
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
}

View file

@ -1,221 +0,0 @@
<?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());
}
}

View file

@ -59,12 +59,7 @@ class ModerationStateBlockTest extends ModerationStateTestBase {
// 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',
];
$edit = ['workflow' => 'editorial'];
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertText(t('Your settings have been saved.'));
@ -78,11 +73,11 @@ class ModerationStateBlockTest extends ModerationStateTestBase {
$this->assertText(t('basic Moderated block has been created.'));
// Place the block in the Sidebar First region.
$instance = array(
$instance = [
'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'));

View file

@ -18,13 +18,7 @@ class ModerationStateNodeTest extends ModerationStateTestBase {
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->adminUser);
$this->createContentTypeFromUi(
'Moderated content',
'moderated_content',
TRUE,
['draft', 'needs_review', 'published'],
'draft'
);
$this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE);
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
}
@ -35,19 +29,11 @@ class ModerationStateNodeTest extends ModerationStateTestBase {
$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) {
$node = $this->getNodeByTitle('moderated content');
if (!$node) {
$this->fail('Test node was not saved correctly.');
return;
}
$node = reset($nodes);
$this->assertEqual('draft', $node->moderation_state->target_id);
$this->assertEqual('draft', $node->moderation_state->value);
$path = 'node/' . $node->id() . '/edit';
// Set up published revision.
@ -56,39 +42,34 @@ class ModerationStateNodeTest extends ModerationStateTestBase {
/* @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);
$this->assertEqual('published', $node->moderation_state->value);
// Verify that the state field is not shown.
$this->assertNoText('Published');
// Delete the node.
$this->drupalPostForm('node/' . $node->id() . '/delete', array(), t('Delete'));
$this->drupalPostForm('node/' . $node->id() . '/delete', [], t('Delete'));
$this->assertText(t('The Moderated content moderated content has been deleted.'));
// Disable content moderation.
$this->drupalPostForm('admin/structure/types/manage/moderated_content/moderation', ['workflow' => ''], t('Save'));
$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->assertOptionSelected('edit-workflow', '');
// Ensure the parent environment is up-to-date.
// @see content_moderation_workflow_insert()
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
// Create a new node.
$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) {
$node = $this->getNodeByTitle('non-moderated content');
if (!$node) {
$this->fail('Non-moderated test node was not saved correctly.');
return;
}
$node = reset($nodes);
$this->assertEqual(NULL, $node->moderation_state->target_id);
$this->assertEqual(NULL, $node->moderation_state->value);
}
/**

View file

@ -42,12 +42,17 @@ class ModerationStateNodeTypeTest extends ModerationStateTestBase {
], 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'
);
// Now enable moderation state, ensuring all the expected links and tabs are
// present.
$this->drupalGet('admin/structure/types');
$this->assertLinkByHref('admin/structure/types/manage/not_moderated/moderation');
$this->drupalGet('admin/structure/types/manage/not_moderated');
$this->assertLinkByHref('admin/structure/types/manage/not_moderated/moderation');
$this->drupalGet('admin/structure/types/manage/not_moderated/moderation');
$this->assertOptionSelected('edit-workflow', '');
$this->assertNoLink('Delete');
$edit['workflow'] = 'editorial';
$this->drupalPostForm(NULL, $edit, t('Save'));
// And make sure it works.
$nodes = \Drupal::entityTypeManager()->getStorage('node')

View file

@ -1,75 +0,0 @@
<?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');
}
}

View file

@ -5,10 +5,12 @@ 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.
*
* @deprecated Scheduled for removal in Drupal 9.0.0.
* Use \Drupal\Tests\content_moderation\Functional\ModerationStateTestBase instead.
*/
abstract class ModerationStateTestBase extends WebTestBase {
@ -30,18 +32,15 @@ abstract class ModerationStateTestBase extends WebTestBase {
* @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',
'administer content moderation',
'access administration pages',
'administer content types',
'administer nodes',
'view latest version',
'view any unpublished content',
'access content overview',
'use editorial transition create_new_draft',
'use editorial transition publish',
];
/**
@ -67,6 +66,21 @@ abstract class ModerationStateTestBase extends WebTestBase {
$this->drupalPlaceBlock('local_actions_block', ['id' => 'actions_block']);
}
/**
* Gets the permission machine name for a transition.
*
* @param string $workflow_id
* The workflow ID.
* @param string $transition_id
* The transition ID.
*
* @return string
* The permission machine name for a transition.
*/
protected function getWorkflowTransitionPermission($workflow_id, $transition_id) {
return 'use ' . $workflow_id . ' transition ' . $transition_id;
}
/**
* Creates a content-type from the UI.
*
@ -76,12 +90,10 @@ abstract class ModerationStateTestBase extends WebTestBase {
* 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.
* @param string $workflow_id
* The workflow to attach to the bundle.
*/
protected function createContentTypeFromUi($content_type_name, $content_type_id, $moderated = FALSE, array $allowed_states = [], $default_state = NULL) {
protected function createContentTypeFromUi($content_type_name, $content_type_id, $moderated = FALSE, $workflow_id = 'editorial') {
$this->drupalGet('admin/structure/types');
$this->clickLink('Add content type');
$edit = [
@ -91,7 +103,7 @@ abstract class ModerationStateTestBase extends WebTestBase {
$this->drupalPostForm(NULL, $edit, t('Save content type'));
if ($moderated) {
$this->enableModerationThroughUi($content_type_id, $allowed_states, $default_state);
$this->enableModerationThroughUi($content_type_id, $workflow_id);
}
}
@ -100,31 +112,16 @@ abstract class ModerationStateTestBase extends WebTestBase {
*
* @param string $content_type_id
* Machine name.
* @param string[] $allowed_states
* Array of allowed state IDs.
* @param string $default_state
* Default state.
* @param string $workflow_id
* The workflow to attach to the bundle.
*/
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'));
protected function enableModerationThroughUi($content_type_id, $workflow_id = 'editorial') {
$edit['workflow'] = $workflow_id;
$this->drupalPostForm('admin/structure/types/manage/' . $content_type_id . '/moderation', $edit, t('Save'));
// Ensure the parent environment is up-to-date.
// @see content_moderation_workflow_insert()
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
}
/**

View file

@ -1,91 +0,0 @@
<?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');
}
}

View file

@ -1,108 +0,0 @@
<?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);
}
}

View file

@ -193,17 +193,14 @@ class ViewsData {
'base' => $content_moderation_state_entity_base_table,
'base field' => 'content_entity_id',
'relationship field' => $entity_type->getKey('id'),
'join_extra' => [
'extra' => [
[
'field' => 'content_entity_type_id',
'value' => $entity_type_id,
],
[
'field' => 'content_entity_revision_id',
'left_field' => $entity_type->getKey('revision'),
],
],
],
'field' => ['default_formatter' => 'content_moderation_state'],
];
$revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable();
@ -215,13 +212,14 @@ class ViewsData {
'base' => $content_moderation_state_entity_revision_base_table,
'base field' => 'content_entity_revision_id',
'relationship field' => $entity_type->getKey('revision'),
'join_extra' => [
'extra' => [
[
'field' => 'content_entity_type_id',
'value' => $entity_type_id,
],
],
],
'field' => ['default_formatter' => 'content_moderation_state'],
];
}