Issue #2861417 by Sam152, timmillwood, larowlan: Correctly handle entity validation of the moderation_state field when trying to save invalid states

8.4.x
Nathaniel Catchpole 2017-07-06 16:31:11 +01:00
parent 612c1fa68c
commit 96dfae63c8
3 changed files with 156 additions and 23 deletions

View File

@ -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';
}

View File

@ -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;
}
// Ignore entities that are being moderated for the first time, such as
// when they existed before moderation was enabled for this entity type.
if ($this->isFirstTimeModeration($entity)) {
return;
}
$original_entity = $this->moderationInformation->getLatestRevision($entity->getEntityTypeId(), $entity->id());
if (!$entity->isDefaultTranslation() && $original_entity->hasTranslation($entity->language()->getId())) {
$original_entity = $original_entity->getTranslation($entity->language()->getId());
}
$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 (!$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;
}
// 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()
]);
}
}
}

View File

@ -96,6 +96,124 @@ class EntityStateChangeValidationTest extends KernelTestBase {
$this->assertEquals('Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>', $violations->get(0)->getMessage());
}
/**
* 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.
*/