summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNathaniel Catchpole2017-07-06 16:31:11 +0100
committerNathaniel Catchpole2017-07-06 16:31:11 +0100
commit96dfae63c89ba8f8133d4f2d877b871da2f12537 (patch)
treee434c7d75278b0fdd2edd7d809bbf8a302cf958e
parent612c1fa68cfca2346c3d981383827278dca9a1ba (diff)
Issue #2861417 by Sam152, timmillwood, larowlan: Correctly handle entity validation of the moderation_state field when trying to save invalid states
-rw-r--r--core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraint.php1
-rw-r--r--core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php54
-rw-r--r--core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php118
3 files changed, 153 insertions, 20 deletions
diff --git a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraint.php b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraint.php
index c2c373f..eebb066 100644
--- a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraint.php
+++ b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraint.php
@@ -15,5 +15,6 @@ use Symfony\Component\Validator\Constraint;
class ModerationStateConstraint extends Constraint {
public $message = 'Invalid state transition from %from to %to';
+ public $invalidStateMessage = 'State %state does not exist on %workflow';
}
diff --git a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php
index b664c65..97ebc93 100644
--- a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php
+++ b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php
@@ -68,7 +68,7 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
- /** @var \Drupal\Core\Entity\EntityInterface $entity */
+ /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $value->getEntity();
// Ignore entities that are not subject to moderation anyway.
@@ -76,29 +76,43 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
return;
}
- // Ignore entities that are being created for the first time.
- if ($entity->isNew()) {
- return;
- }
+ $workflow = $this->moderationInformation->getWorkflowForEntity($entity);
- // Ignore entities that are being moderated for the first time, such as
- // when they existed before moderation was enabled for this entity type.
- if ($this->isFirstTimeModeration($entity)) {
+ if (!$workflow->hasState($entity->moderation_state->value)) {
+ // If the state we are transitioning to doesn't exist, we can't validate
+ // the transitions for this entity further.
+ $this->context->addViolation($constraint->invalidStateMessage, [
+ '%state' => $entity->moderation_state->value,
+ '%workflow' => $workflow->label(),
+ ]);
return;
}
- $original_entity = $this->moderationInformation->getLatestRevision($entity->getEntityTypeId(), $entity->id());
- if (!$entity->isDefaultTranslation() && $original_entity->hasTranslation($entity->language()->getId())) {
- $original_entity = $original_entity->getTranslation($entity->language()->getId());
- }
-
- $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 (!$original_state->canTransitionTo($new_state->id())) {
- $this->context->addViolation($constraint->message, ['%from' => $original_state->label(), '%to' => $new_state->label()]);
+ // If a new state is being set and there is an existing state, validate
+ // there is a valid transition between them.
+ 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->hasState($original_entity->moderation_state->value)) {
+ return;
+ }
+
+ $new_state = $workflow->getState($entity->moderation_state->value);
+ $original_state = $workflow->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()
+ ]);
+ }
}
}
diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
index 7c4f97d..c8ce61b 100644
--- a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
+++ b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
@@ -97,6 +97,124 @@ class EntityStateChangeValidationTest extends KernelTestBase {
}
/**
+ * Test validation with an invalid state.
+ */
+ public function testInvalidState() {
+ $node_type = NodeType::create([
+ 'type' => 'example',
+ ]);
+ $node_type->save();
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
+ $workflow->save();
+
+ $node = Node::create([
+ 'type' => 'example',
+ 'title' => 'Test title',
+ ]);
+ $node->moderation_state->value = 'invalid_state';
+ $violations = $node->validate();
+
+ $this->assertCount(1, $violations);
+ $this->assertEquals('State <em class="placeholder">invalid_state</em> does not exist on <em class="placeholder">Editorial workflow</em>', $violations->get(0)->getMessage());
+ }
+
+ /**
+ * Test validation with content that has no initial state or an invalid state.
+ */
+ public function testInvalidStateWithoutExisting() {
+ // Create content without moderation enabled for the content type.
+ $node_type = NodeType::create([
+ 'type' => 'example',
+ ]);
+ $node_type->save();
+ $node = Node::create([
+ 'type' => 'example',
+ 'title' => 'Test title',
+ ]);
+ $node->save();
+
+ // Enable moderation to test validation on existing content, with no
+ // explicit state.
+ $workflow = Workflow::load('editorial');
+ $workflow->addState('deleted_state', 'Deleted state');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
+ $workflow->save();
+
+ // Validate the invalid state.
+ $node->moderation_state->value = 'invalid_state';
+ $violations = $node->validate();
+ $this->assertCount(1, $violations);
+
+ // Assign the node to a state we're going to delete.
+ $node->moderation_state->value = 'deleted_state';
+ $node->save();
+
+ // Delete the state so $node->original contains an invalid state when
+ // validating.
+ $workflow->deleteState('deleted_state');
+ $workflow->save();
+ $node->moderation_state->value = 'draft';
+ $violations = $node->validate();
+ $this->assertCount(0, $violations);
+ }
+
+ /**
+ * Test state transition validation with multiple languages.
+ */
+ public function testInvalidStateMultilingual() {
+ ConfigurableLanguage::createFromLangcode('fr')->save();
+ $node_type = NodeType::create([
+ 'type' => 'example',
+ ]);
+ $node_type->save();
+
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
+ $workflow->save();
+
+ $node = Node::create([
+ 'type' => 'example',
+ 'title' => 'English Published Node',
+ 'langcode' => 'en',
+ 'moderation_state' => 'published',
+ ]);
+ $node->save();
+
+ $node_fr = $node->addTranslation('fr');
+ $node_fr->setTitle('French Published Node');
+ $node_fr->save();
+ $this->assertEquals('published', $node_fr->moderation_state->value);
+
+ // Create a forward revision of the original node.
+ $node->moderation_state = 'draft';
+ $node->setNewRevision(TRUE);
+ $node->isDefaultRevision(FALSE);
+ $node->save();
+
+ // For the forward english revision, there should be a violation from draft
+ // to archived.
+ $node->moderation_state = 'archived';
+ $violations = $node->validate();
+ $this->assertCount(1, $violations);
+ $this->assertEquals('Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>', $violations->get(0)->getMessage());
+
+ // From the default french published revision, there should be none.
+ $node_fr = Node::load($node->id())->getTranslation('fr');
+ $this->assertEquals('published', $node_fr->moderation_state->value);
+ $node_fr->moderation_state = 'archived';
+ $violations = $node_fr->validate();
+ $this->assertCount(0, $violations);
+
+ // From the latest french revision, there should also be no violation.
+ $node_fr = $node->getTranslation('fr');
+ $this->assertEquals('published', $node_fr->moderation_state->value);
+ $node_fr->moderation_state = 'archived';
+ $violations = $node_fr->validate();
+ $this->assertCount(0, $violations);
+ }
+
+ /**
* Tests that content without prior moderation information can be moderated.
*/
public function testLegacyContent() {