Issue #2861417 by Sam152, timmillwood, larowlan: Correctly handle entity validation of the moderation_state field when trying to save invalid states
parent
612c1fa68c
commit
96dfae63c8
|
@ -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';
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue