<?php declare(strict_types=1);
namespace Shopware\Core\Content\Rule;
use Shopware\Core\Content\Rule\Aggregate\RuleCondition\RuleConditionCollection;
use Shopware\Core\Content\Rule\Aggregate\RuleCondition\RuleConditionDefinition;
use Shopware\Core\Content\Rule\Aggregate\RuleCondition\RuleConditionEntity;
use Shopware\Core\Framework\App\Aggregate\AppScriptCondition\AppScriptConditionEntity;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Exception\UnsupportedCommandTypeException;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\DeleteCommand;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\InsertCommand;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommand;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteException;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Rule\Collector\RuleConditionRegistry;
use Shopware\Core\Framework\Rule\Exception\InvalidConditionException;
use Shopware\Core\Framework\Rule\ScriptRule;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\Framework\Validation\WriteConstraintViolationException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @deprecated tag:v6.5.0 - reason:becomes-internal - EventSubscribers will become internal in v6.5.0
*/
#[Package('business-ops')]
class RuleValidator implements EventSubscriberInterface
{
private ValidatorInterface $validator;
private RuleConditionRegistry $ruleConditionRegistry;
private EntityRepositoryInterface $ruleConditionRepository;
private EntityRepositoryInterface $appScriptConditionRepository;
/**
* @internal
*/
public function __construct(
ValidatorInterface $validator,
RuleConditionRegistry $ruleConditionRegistry,
EntityRepositoryInterface $ruleConditionRepository,
EntityRepositoryInterface $appScriptConditionRepository
) {
$this->validator = $validator;
$this->ruleConditionRegistry = $ruleConditionRegistry;
$this->ruleConditionRepository = $ruleConditionRepository;
$this->appScriptConditionRepository = $appScriptConditionRepository;
}
public static function getSubscribedEvents(): array
{
return [
PreWriteValidationEvent::class => 'preValidate',
];
}
/**
* @throws UnsupportedCommandTypeException
*/
public function preValidate(PreWriteValidationEvent $event): void
{
$writeException = $event->getExceptions();
$commands = $event->getCommands();
$updateQueue = [];
foreach ($commands as $command) {
if ($command->getDefinition()->getClass() !== RuleConditionDefinition::class) {
continue;
}
if ($command instanceof DeleteCommand) {
continue;
}
if ($command instanceof InsertCommand) {
$this->validateCondition(null, $command, $writeException, $event->getContext());
continue;
}
if ($command instanceof UpdateCommand) {
$updateQueue[] = $command;
continue;
}
throw new UnsupportedCommandTypeException($command);
}
if (!empty($updateQueue)) {
$this->validateUpdateCommands($updateQueue, $writeException, $event->getContext());
}
}
private function validateCondition(
?RuleConditionEntity $condition,
WriteCommand $command,
WriteException $writeException,
Context $context
): void {
$payload = $command->getPayload();
$violationList = new ConstraintViolationList();
$type = $this->getConditionType($condition, $payload);
if ($type === null) {
return;
}
try {
$ruleInstance = $this->ruleConditionRegistry->getRuleInstance($type);
} catch (InvalidConditionException $e) {
$violation = $this->buildViolation(
'This {{ value }} is not a valid condition type.',
['{{ value }}' => $type],
'/type',
'CONTENT__INVALID_RULE_TYPE_EXCEPTION'
);
$violationList->add($violation);
$writeException->add(new WriteConstraintViolationException($violationList, $command->getPath()));
return;
}
$value = $this->getConditionValue($condition, $payload);
$ruleInstance->assign($value);
if ($ruleInstance instanceof ScriptRule) {
$this->setScriptConstraints($ruleInstance, $condition, $payload, $context);
}
$this->validateConsistence(
$ruleInstance->getConstraints(),
$value,
$violationList
);
if ($violationList->count() > 0) {
$writeException->add(new WriteConstraintViolationException($violationList, $command->getPath()));
}
}
/**
* @param array<mixed> $payload
*/
private function getConditionType(?RuleConditionEntity $condition, array $payload): ?string
{
$type = $condition !== null ? $condition->getType() : null;
if (\array_key_exists('type', $payload)) {
$type = $payload['type'];
}
return $type;
}
/**
* @param array<mixed> $payload
*
* @return array<mixed>
*/
private function getConditionValue(?RuleConditionEntity $condition, array $payload): array
{
$value = $condition !== null ? $condition->getValue() : [];
if (isset($payload['value']) && $payload['value'] !== null) {
$value = json_decode($payload['value'], true);
}
return $value ?? [];
}
/**
* @param array<string, array<Constraint>> $fieldValidations
* @param array<mixed> $payload
*/
private function validateConsistence(array $fieldValidations, array $payload, ConstraintViolationList $violationList): void
{
foreach ($fieldValidations as $fieldName => $validations) {
$violationList->addAll(
$this->validator->startContext()
->atPath('/value/' . $fieldName)
->validate($payload[$fieldName] ?? null, $validations)
->getViolations()
);
}
foreach ($payload as $fieldName => $_value) {
if (!\array_key_exists($fieldName, $fieldValidations) && $fieldName !== '_name') {
$violationList->add(
$this->buildViolation(
'The property "{{ fieldName }}" is not allowed.',
['{{ fieldName }}' => $fieldName],
'/value/' . $fieldName
)
);
}
}
}
/**
* @param array<UpdateCommand> $commandQueue
*/
private function validateUpdateCommands(
array $commandQueue,
WriteException $writeException,
Context $context
): void {
$conditions = $this->getSavedConditions($commandQueue, $context);
foreach ($commandQueue as $command) {
$id = Uuid::fromBytesToHex($command->getPrimaryKey()['id']);
$condition = $conditions->get($id);
$this->validateCondition($condition, $command, $writeException, $context);
}
}
/**
* @param array<UpdateCommand> $commandQueue
*/
private function getSavedConditions(array $commandQueue, Context $context): RuleConditionCollection
{
$ids = array_map(function ($command) {
$uuidBytes = $command->getPrimaryKey()['id'];
return Uuid::fromBytesToHex($uuidBytes);
}, $commandQueue);
$criteria = new Criteria($ids);
$criteria->setLimit(null);
/** @var RuleConditionCollection $entities */
$entities = $this->ruleConditionRepository->search($criteria, $context)->getEntities();
return $entities;
}
/**
* @param array<int|string> $parameters
*/
private function buildViolation(
string $messageTemplate,
array $parameters,
?string $propertyPath = null,
?string $code = null
): ConstraintViolationInterface {
return new ConstraintViolation(
str_replace(array_keys($parameters), array_values($parameters), $messageTemplate),
$messageTemplate,
$parameters,
null,
$propertyPath,
null,
null,
$code
);
}
/**
* @param array<mixed> $payload
*/
private function setScriptConstraints(
ScriptRule $ruleInstance,
?RuleConditionEntity $condition,
array $payload,
Context $context
): void {
$script = null;
if (isset($payload['script_id'])) {
$scriptId = Uuid::fromBytesToHex($payload['script_id']);
$script = $this->appScriptConditionRepository->search(new Criteria([$scriptId]), $context)->get($scriptId);
} elseif ($condition && $condition->getAppScriptCondition()) {
$script = $condition->getAppScriptCondition();
}
if (!$script instanceof AppScriptConditionEntity || !\is_array($script->getConstraints())) {
return;
}
$ruleInstance->setConstraints($script->getConstraints());
}
}