updated core to 8.6.2

This commit is contained in:
2018-10-23 11:50:40 +02:00
parent afcb9d9c34
commit 5c71ef4c46
34 changed files with 1083 additions and 566 deletions
@@ -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';
}
@@ -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>'],
],
];
}
}