ModerationStateConstraintValidator.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. <?php
  2. namespace Drupal\content_moderation\Plugin\Validation\Constraint;
  3. use Drupal\content_moderation\StateTransitionValidationInterface;
  4. use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
  5. use Drupal\Core\Entity\ContentEntityInterface;
  6. use Drupal\Core\Entity\EntityInterface;
  7. use Drupal\Core\Entity\EntityTypeManagerInterface;
  8. use Drupal\content_moderation\ModerationInformationInterface;
  9. use Drupal\Core\Session\AccountInterface;
  10. use Symfony\Component\DependencyInjection\ContainerInterface;
  11. use Symfony\Component\Validator\Constraint;
  12. use Symfony\Component\Validator\ConstraintValidator;
  13. /**
  14. * Checks if a moderation state transition is valid.
  15. */
  16. class ModerationStateConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
  17. /**
  18. * The entity type manager.
  19. *
  20. * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  21. */
  22. private $entityTypeManager;
  23. /**
  24. * The moderation info.
  25. *
  26. * @var \Drupal\content_moderation\ModerationInformationInterface
  27. */
  28. protected $moderationInformation;
  29. /**
  30. * The current user.
  31. *
  32. * @var \Drupal\Core\Session\AccountInterface
  33. */
  34. protected $currentUser;
  35. /**
  36. * The state transition validation service.
  37. *
  38. * @var \Drupal\content_moderation\StateTransitionValidationInterface
  39. */
  40. protected $stateTransitionValidation;
  41. /**
  42. * Creates a new ModerationStateConstraintValidator instance.
  43. *
  44. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
  45. * The entity type manager.
  46. * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
  47. * The moderation information.
  48. * @param \Drupal\Core\Session\AccountInterface $current_user
  49. * The current user.
  50. * @param \Drupal\content_moderation\StateTransitionValidationInterface $state_transition_validation
  51. * The state transition validation service.
  52. */
  53. public function __construct(EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_information, AccountInterface $current_user, StateTransitionValidationInterface $state_transition_validation) {
  54. $this->entityTypeManager = $entity_type_manager;
  55. $this->moderationInformation = $moderation_information;
  56. $this->currentUser = $current_user;
  57. $this->stateTransitionValidation = $state_transition_validation;
  58. }
  59. /**
  60. * {@inheritdoc}
  61. */
  62. public static function create(ContainerInterface $container) {
  63. return new static(
  64. $container->get('entity_type.manager'),
  65. $container->get('content_moderation.moderation_information'),
  66. $container->get('current_user'),
  67. $container->get('content_moderation.state_transition_validation')
  68. );
  69. }
  70. /**
  71. * {@inheritdoc}
  72. */
  73. public function validate($value, Constraint $constraint) {
  74. /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
  75. $entity = $value->getEntity();
  76. // Ignore entities that are not subject to moderation anyway.
  77. if (!$this->moderationInformation->isModeratedEntity($entity)) {
  78. return;
  79. }
  80. $workflow = $this->moderationInformation->getWorkflowForEntity($entity);
  81. if (!$workflow->getTypePlugin()->hasState($entity->moderation_state->value)) {
  82. // If the state we are transitioning to doesn't exist, we can't validate
  83. // the transitions for this entity further.
  84. $this->context->addViolation($constraint->invalidStateMessage, [
  85. '%state' => $entity->moderation_state->value,
  86. '%workflow' => $workflow->label(),
  87. ]);
  88. return;
  89. }
  90. $new_state = $workflow->getTypePlugin()->getState($entity->moderation_state->value);
  91. $original_state = $this->getOriginalOrInitialState($entity);
  92. // If a new state is being set and there is an existing state, validate
  93. // there is a valid transition between them.
  94. if (!$original_state->canTransitionTo($new_state->id())) {
  95. $this->context->addViolation($constraint->message, [
  96. '%from' => $original_state->label(),
  97. '%to' => $new_state->label(),
  98. ]);
  99. }
  100. else {
  101. // If we're sure the transition exists, make sure the user has permission
  102. // to use it.
  103. if (!$this->stateTransitionValidation->isTransitionValid($workflow, $original_state, $new_state, $this->currentUser)) {
  104. $this->context->addViolation($constraint->invalidTransitionAccess, [
  105. '%original_state' => $original_state->label(),
  106. '%new_state' => $new_state->label(),
  107. ]);
  108. }
  109. }
  110. }
  111. /**
  112. * Gets the original or initial state of the given entity.
  113. *
  114. * When a state is being validated, the original state is used to validate
  115. * that a valid transition exists for target state and the user has access
  116. * to the transition between those two states. If the entity has been
  117. * moderated before, we can load the original unmodified revision and
  118. * translation for this state.
  119. *
  120. * If the entity is new we need to load the initial state from the workflow.
  121. * Even if a value was assigned to the moderation_state field, the initial
  122. * state is used to compute an appropriate transition for the purposes of
  123. * validation.
  124. *
  125. * @return \Drupal\workflows\StateInterface
  126. * The original or default moderation state.
  127. */
  128. protected function getOriginalOrInitialState(ContentEntityInterface $entity) {
  129. $state = NULL;
  130. $workflow_type = $this->moderationInformation->getWorkflowForEntity($entity)->getTypePlugin();
  131. if (!$entity->isNew() && !$this->isFirstTimeModeration($entity)) {
  132. $original_entity = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->loadRevision($entity->getLoadedRevisionId());
  133. if (!$entity->isDefaultTranslation() && $original_entity->hasTranslation($entity->language()->getId())) {
  134. $original_entity = $original_entity->getTranslation($entity->language()->getId());
  135. }
  136. if ($workflow_type->hasState($original_entity->moderation_state->value)) {
  137. $state = $workflow_type->getState($original_entity->moderation_state->value);
  138. }
  139. }
  140. return $state ?: $workflow_type->getInitialState($entity);
  141. }
  142. /**
  143. * Determines if this entity is being moderated for the first time.
  144. *
  145. * If the previous version of the entity has no moderation state, we assume
  146. * that means it predates the presence of moderation states.
  147. *
  148. * @param \Drupal\Core\Entity\EntityInterface $entity
  149. * The entity being moderated.
  150. *
  151. * @return bool
  152. * TRUE if this is the entity's first time being moderated, FALSE otherwise.
  153. */
  154. protected function isFirstTimeModeration(EntityInterface $entity) {
  155. $original_entity = $this->moderationInformation->getLatestRevision($entity->getEntityTypeId(), $entity->id());
  156. if ($original_entity) {
  157. $original_id = $original_entity->moderation_state;
  158. }
  159. return !($entity->moderation_state && $original_entity && $original_id);
  160. }
  161. }