updated core to 8.6.2
This commit is contained in:
+1
@@ -16,5 +16,6 @@ class ModerationStateConstraint extends Constraint {
|
||||
|
||||
public $message = 'Invalid state transition from %from to %to';
|
||||
public $invalidStateMessage = 'State %state does not exist on %workflow workflow';
|
||||
public $invalidTransitionAccess = 'You do not have access to transition from %original_state to %new_state';
|
||||
|
||||
}
|
||||
|
||||
+71
-19
@@ -2,10 +2,13 @@
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\content_moderation\StateTransitionValidationInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
@@ -29,6 +32,20 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
|
||||
*/
|
||||
protected $moderationInformation;
|
||||
|
||||
/**
|
||||
* The current user.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* The state transition validation service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\StateTransitionValidationInterface
|
||||
*/
|
||||
protected $stateTransitionValidation;
|
||||
|
||||
/**
|
||||
* Creates a new ModerationStateConstraintValidator instance.
|
||||
*
|
||||
@@ -36,10 +53,16 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
|
||||
* The entity type manager.
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
|
||||
* The moderation information.
|
||||
* @param \Drupal\Core\Session\AccountInterface $current_user
|
||||
* The current user.
|
||||
* @param \Drupal\content_moderation\StateTransitionValidationInterface $state_transition_validation
|
||||
* The state transition validation service.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_information) {
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_information, AccountInterface $current_user, StateTransitionValidationInterface $state_transition_validation) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->moderationInformation = $moderation_information;
|
||||
$this->currentUser = $current_user;
|
||||
$this->stateTransitionValidation = $state_transition_validation;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,7 +71,9 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('content_moderation.moderation_information')
|
||||
$container->get('content_moderation.moderation_information'),
|
||||
$container->get('current_user'),
|
||||
$container->get('content_moderation.state_transition_validation')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,32 +101,59 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
|
||||
return;
|
||||
}
|
||||
|
||||
$new_state = $workflow->getTypePlugin()->getState($entity->moderation_state->value);
|
||||
$original_state = $this->getOriginalOrInitialState($entity);
|
||||
|
||||
// If a new state is being set and there is an existing state, validate
|
||||
// there is a valid transition between them.
|
||||
if (!$original_state->canTransitionTo($new_state->id())) {
|
||||
$this->context->addViolation($constraint->message, [
|
||||
'%from' => $original_state->label(),
|
||||
'%to' => $new_state->label(),
|
||||
]);
|
||||
}
|
||||
else {
|
||||
// If we're sure the transition exists, make sure the user has permission
|
||||
// to use it.
|
||||
if (!$this->stateTransitionValidation->isTransitionValid($workflow, $original_state, $new_state, $this->currentUser)) {
|
||||
$this->context->addViolation($constraint->invalidTransitionAccess, [
|
||||
'%original_state' => $original_state->label(),
|
||||
'%new_state' => $new_state->label(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the original or initial state of the given entity.
|
||||
*
|
||||
* When a state is being validated, the original state is used to validate
|
||||
* that a valid transition exists for target state and the user has access
|
||||
* to the transition between those two states. If the entity has been
|
||||
* moderated before, we can load the original unmodified revision and
|
||||
* translation for this state.
|
||||
*
|
||||
* If the entity is new we need to load the initial state from the workflow.
|
||||
* Even if a value was assigned to the moderation_state field, the initial
|
||||
* state is used to compute an appropriate transition for the purposes of
|
||||
* validation.
|
||||
*
|
||||
* @return \Drupal\workflows\StateInterface
|
||||
* The original or default moderation state.
|
||||
*/
|
||||
protected function getOriginalOrInitialState(ContentEntityInterface $entity) {
|
||||
$state = NULL;
|
||||
$workflow_type = $this->moderationInformation->getWorkflowForEntity($entity)->getTypePlugin();
|
||||
if (!$entity->isNew() && !$this->isFirstTimeModeration($entity)) {
|
||||
$original_entity = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->loadRevision($entity->getLoadedRevisionId());
|
||||
if (!$entity->isDefaultTranslation() && $original_entity->hasTranslation($entity->language()->getId())) {
|
||||
$original_entity = $original_entity->getTranslation($entity->language()->getId());
|
||||
}
|
||||
|
||||
// If the state of the original entity doesn't exist on the workflow,
|
||||
// we cannot do any further validation of transitions, because none will
|
||||
// be setup for a state that doesn't exist. Instead allow any state to
|
||||
// take its place.
|
||||
if (!$workflow->getTypePlugin()->hasState($original_entity->moderation_state->value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$new_state = $workflow->getTypePlugin()->getState($entity->moderation_state->value);
|
||||
$original_state = $workflow->getTypePlugin()->getState($original_entity->moderation_state->value);
|
||||
|
||||
if (!$original_state->canTransitionTo($new_state->id())) {
|
||||
$this->context->addViolation($constraint->message, [
|
||||
'%from' => $original_state->label(),
|
||||
'%to' => $new_state->label(),
|
||||
]);
|
||||
if ($workflow_type->hasState($original_entity->moderation_state->value)) {
|
||||
$state = $workflow_type->getState($original_entity->moderation_state->value);
|
||||
}
|
||||
}
|
||||
return $state ?: $workflow_type->getInitialState($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,9 @@ namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\workflows\StateInterface;
|
||||
use Drupal\workflows\Transition;
|
||||
use Drupal\workflows\WorkflowInterface;
|
||||
|
||||
/**
|
||||
* Validates whether a certain state transition is allowed.
|
||||
@@ -47,4 +49,12 @@ class StateTransitionValidation implements StateTransitionValidationInterface {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isTransitionValid(WorkflowInterface $workflow, StateInterface $original_state, StateInterface $new_state, AccountInterface $user) {
|
||||
$transition = $workflow->getTypePlugin()->getTransitionFromStateToState($original_state->id(), $new_state->id());
|
||||
return $user->hasPermission('use ' . $workflow->id() . ' transition ' . $transition->id());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\workflows\StateInterface;
|
||||
use Drupal\workflows\WorkflowInterface;
|
||||
|
||||
/**
|
||||
* Validates whether a certain state transition is allowed.
|
||||
@@ -23,4 +25,21 @@ interface StateTransitionValidationInterface {
|
||||
*/
|
||||
public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user);
|
||||
|
||||
/**
|
||||
* Checks if a transition between two states if valid for the given user.
|
||||
*
|
||||
* @param \Drupal\workflows\WorkflowInterface $workflow
|
||||
* The workflow entity.
|
||||
* @param \Drupal\workflows\StateInterface $original_state
|
||||
* The original workflow state.
|
||||
* @param \Drupal\workflows\StateInterface $new_state
|
||||
* The new workflow state.
|
||||
* @param \Drupal\Core\Session\AccountInterface $user
|
||||
* The user to validate.
|
||||
*
|
||||
* @return bool
|
||||
* Returns TRUE if transition is valid, otherwise FALSE.
|
||||
*/
|
||||
public function isTransitionValid(WorkflowInterface $workflow, StateInterface $original_state, StateInterface $new_state, AccountInterface $user);
|
||||
|
||||
}
|
||||
|
||||
@@ -158,32 +158,15 @@ class ModerationStateNodeTest extends ModerationStateTestBase {
|
||||
]);
|
||||
$this->drupalLogin($limited_user);
|
||||
|
||||
// Check the user can add content, but can't see the moderation state
|
||||
// select.
|
||||
// Check the user can see the content entity form, but can't see the
|
||||
// moderation state select or save the entity form.
|
||||
$this->drupalGet('node/add/moderated_content');
|
||||
$session_assert->statusCodeEquals(200);
|
||||
$session_assert->fieldNotExists('moderation_state[0][state]');
|
||||
$this->drupalPostForm(NULL, [
|
||||
'title[0][value]' => 'moderated content',
|
||||
], 'Save');
|
||||
|
||||
// Manually move the content to archived because the user doesn't have
|
||||
// permission to do this.
|
||||
$node = $this->getNodeByTitle('moderated content');
|
||||
$node->moderation_state->value = 'archived';
|
||||
$node->save();
|
||||
|
||||
// Check the user can see the current state but not the select.
|
||||
$this->drupalGet('node/' . $node->id() . '/edit');
|
||||
$session_assert->statusCodeEquals(200);
|
||||
$session_assert->pageTextContains('Archived');
|
||||
$session_assert->fieldNotExists('moderation_state[0][state]');
|
||||
$this->drupalPostForm(NULL, [], 'Save');
|
||||
|
||||
// When saving they should still be on the edit form, and see the validation
|
||||
// error message.
|
||||
$session_assert->pageTextContains('Edit Moderated content moderated content');
|
||||
$session_assert->pageTextContains('Invalid state transition from Archived to Archived');
|
||||
$session_assert->pageTextContains('You do not have access to transition from Draft to Draft');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
|
||||
use Drupal\Tests\user\Traits\UserCreationTrait;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\Plugin\Validation\Constraint\ModerationStateConstraintValidator
|
||||
@@ -15,6 +16,7 @@ use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
|
||||
class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
|
||||
use ContentModerationTestTrait;
|
||||
use UserCreationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
@@ -29,6 +31,13 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
'workflows',
|
||||
];
|
||||
|
||||
/**
|
||||
* An admin user.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -40,6 +49,9 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
$this->installEntitySchema('user');
|
||||
$this->installEntitySchema('content_moderation_state');
|
||||
$this->installConfig('content_moderation');
|
||||
$this->installSchema('system', ['sequences']);
|
||||
|
||||
$this->adminUser = $this->createUser(array_keys($this->container->get('user.permissions')->getPermissions()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +60,8 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
* @covers ::validate
|
||||
*/
|
||||
public function testValidTransition() {
|
||||
$this->setCurrentUser($this->adminUser);
|
||||
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
@@ -76,6 +90,8 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
* @covers ::validate
|
||||
*/
|
||||
public function testInvalidTransition() {
|
||||
$this->setCurrentUser($this->adminUser);
|
||||
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
@@ -125,6 +141,7 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
* Test validation with content that has no initial state or an invalid state.
|
||||
*/
|
||||
public function testInvalidStateWithoutExisting() {
|
||||
$this->setCurrentUser($this->adminUser);
|
||||
// Create content without moderation enabled for the content type.
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
@@ -156,15 +173,24 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
// validating.
|
||||
$workflow->getTypePlugin()->deleteState('deleted_state');
|
||||
$workflow->save();
|
||||
|
||||
// When there is an invalid state, the content will revert to "draft". This
|
||||
// will allow a draft to draft transition.
|
||||
$node->moderation_state->value = 'draft';
|
||||
$violations = $node->validate();
|
||||
$this->assertCount(0, $violations);
|
||||
// This will disallow a draft to archived transition.
|
||||
$node->moderation_state->value = 'archived';
|
||||
$violations = $node->validate();
|
||||
$this->assertCount(1, $violations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test state transition validation with multiple languages.
|
||||
*/
|
||||
public function testInvalidStateMultilingual() {
|
||||
$this->setCurrentUser($this->adminUser);
|
||||
|
||||
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
@@ -220,6 +246,8 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
* Tests that content without prior moderation information can be moderated.
|
||||
*/
|
||||
public function testExistingContentWithNoModeration() {
|
||||
$this->setCurrentUser($this->adminUser);
|
||||
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
@@ -254,6 +282,8 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
* Tests that content without prior moderation information can be translated.
|
||||
*/
|
||||
public function testExistingMultilingualContentWithNoModeration() {
|
||||
$this->setCurrentUser($this->adminUser);
|
||||
|
||||
// Enable French.
|
||||
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||
|
||||
@@ -293,4 +323,81 @@ class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
$node_fr->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider transitionAccessValidationTestCases
|
||||
*/
|
||||
public function testTransitionAccessValidation($permissions, $target_state, $messages) {
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->save();
|
||||
$workflow = $this->createEditorialWorkflow();
|
||||
$workflow->getTypePlugin()->addState('foo', 'Foo');
|
||||
$workflow->getTypePlugin()->addTransition('draft_to_foo', 'Draft to foo', ['draft'], 'foo');
|
||||
$workflow->getTypePlugin()->addTransition('foo_to_foo', 'Foo to foo', ['foo'], 'foo');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
|
||||
$workflow->save();
|
||||
|
||||
$this->setCurrentUser($this->createUser($permissions));
|
||||
|
||||
$node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Test content',
|
||||
'moderation_state' => $target_state,
|
||||
]);
|
||||
$this->assertTrue($node->isNew());
|
||||
$violations = $node->validate();
|
||||
$this->assertCount(count($messages), $violations);
|
||||
foreach ($messages as $i => $message) {
|
||||
$this->assertEquals($message, $violations->get($i)->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cases for ::testTransitionAccessValidation.
|
||||
*/
|
||||
public function transitionAccessValidationTestCases() {
|
||||
return [
|
||||
'Invalid transition, no permissions validated' => [
|
||||
[],
|
||||
'archived',
|
||||
['Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>'],
|
||||
],
|
||||
'Valid transition, missing permission' => [
|
||||
[],
|
||||
'published',
|
||||
['You do not have access to transition from <em class="placeholder">Draft</em> to <em class="placeholder">Published</em>'],
|
||||
],
|
||||
'Valid transition, granted published permission' => [
|
||||
['use editorial transition publish'],
|
||||
'published',
|
||||
[],
|
||||
],
|
||||
'Valid transition, granted draft permission' => [
|
||||
['use editorial transition create_new_draft'],
|
||||
'draft',
|
||||
[],
|
||||
],
|
||||
'Valid transition, incorrect permission granted' => [
|
||||
['use editorial transition create_new_draft'],
|
||||
'published',
|
||||
['You do not have access to transition from <em class="placeholder">Draft</em> to <em class="placeholder">Published</em>'],
|
||||
],
|
||||
// Test with an additional state and set of transitions, since the
|
||||
// "published" transition can start from either "draft" or "published", it
|
||||
// does not capture bugs that fail to correctly distinguish the initial
|
||||
// workflow state from the set state of a new entity.
|
||||
'Valid transition, granted foo permission' => [
|
||||
['use editorial transition draft_to_foo'],
|
||||
'foo',
|
||||
[],
|
||||
],
|
||||
'Valid transition, incorrect foo permission granted' => [
|
||||
['use editorial transition foo_to_foo'],
|
||||
'foo',
|
||||
['You do not have access to transition from <em class="placeholder">Draft</em> to <em class="placeholder">Foo</em>'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user