init
This commit is contained in:
+34
@@ -0,0 +1,34 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationGraph;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
|
||||
class AutomationNode {
|
||||
/** @var Step */
|
||||
private $step;
|
||||
|
||||
/** @var array */
|
||||
private $parents;
|
||||
|
||||
/* @param Step[] $parents */
|
||||
public function __construct(
|
||||
Step $step,
|
||||
array $parents
|
||||
) {
|
||||
$this->step = $step;
|
||||
$this->parents = $parents;
|
||||
}
|
||||
|
||||
public function getStep(): Step {
|
||||
return $this->step;
|
||||
}
|
||||
|
||||
/** @return Step[] */
|
||||
public function getParents(): array {
|
||||
return $this->parents;
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationGraph;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
|
||||
interface AutomationNodeVisitor {
|
||||
public function initialize(Automation $automation): void;
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void;
|
||||
|
||||
public function complete(Automation $automation): void;
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationGraph;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use Generator;
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Exceptions\UnexpectedValueException;
|
||||
|
||||
class AutomationWalker {
|
||||
/** @param AutomationNodeVisitor[] $visitors */
|
||||
public function walk(Automation $automation, array $visitors = []): void {
|
||||
$steps = $automation->getSteps();
|
||||
$root = $steps['root'] ?? null;
|
||||
if (!$root) {
|
||||
throw Exceptions::automationStructureNotValid(__("Automation must contain a 'root' step", 'mailpoet'), 'no-root');
|
||||
}
|
||||
|
||||
foreach ($visitors as $visitor) {
|
||||
$visitor->initialize($automation);
|
||||
}
|
||||
|
||||
foreach ($this->walkStepsDepthFirstPreOrder($steps, $root) as $record) {
|
||||
[$step, $parents] = $record;
|
||||
foreach ($visitors as $visitor) {
|
||||
$visitor->visitNode($automation, new AutomationNode($step, array_values($parents)));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($visitors as $visitor) {
|
||||
$visitor->complete($automation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string|int, Step> $steps
|
||||
* @return Generator<array{0: Step, 1: array<string|int, Step>}>
|
||||
*/
|
||||
private function walkStepsDepthFirstPreOrder(array $steps, Step $root): Generator {
|
||||
/** @var array{0: Step, 1: array<string|int, Step>}[] $stack */
|
||||
$stack = [
|
||||
[$root, []],
|
||||
];
|
||||
|
||||
do {
|
||||
$record = array_pop($stack);
|
||||
if (!$record) {
|
||||
throw new InvalidStateException();
|
||||
}
|
||||
yield $record;
|
||||
[$step, $parents] = $record;
|
||||
|
||||
foreach (array_reverse($step->getNextSteps()) as $nextStepData) {
|
||||
$nextStepId = $nextStepData->getId();
|
||||
if (!$nextStepId) {
|
||||
continue; // empty edge
|
||||
}
|
||||
$nextStep = $steps[$nextStepId] ?? null;
|
||||
if (!$nextStep) {
|
||||
throw $this->createStepNotFoundException($nextStepId, $step->getId());
|
||||
}
|
||||
|
||||
$nextStepParents = array_merge($parents, [$step->getId() => $step]);
|
||||
if (isset($nextStepParents[$nextStepId])) {
|
||||
continue; // cycle detected, do not enter the path again
|
||||
}
|
||||
array_push($stack, [$nextStep, $nextStepParents]);
|
||||
}
|
||||
} while (count($stack) > 0);
|
||||
}
|
||||
|
||||
private function createStepNotFoundException(string $stepId, string $parentStepId): UnexpectedValueException {
|
||||
return Exceptions::automationStructureNotValid(
|
||||
// translators: %1$s is ID of the step not found, %2$s is ID of the step that references it
|
||||
sprintf(
|
||||
__("Step with ID '%1\$s' not found (referenced from '%2\$s')", 'mailpoet'),
|
||||
$stepId,
|
||||
$parentStepId
|
||||
),
|
||||
'step-not-found'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class AtLeastOneTriggerRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'at-least-one-trigger';
|
||||
|
||||
/** @var bool */
|
||||
private $triggerFound = false;
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
$this->triggerFound = false;
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
if ($node->getStep()->getType() === Step::TYPE_TRIGGER) {
|
||||
$this->triggerFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
if (!$automation->needsFullValidation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->triggerFound) {
|
||||
return;
|
||||
}
|
||||
throw Exceptions::automationStructureNotValid(__('There must be at least one trigger in the automation.', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class ConsistentStepMapRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'consistent-step-map';
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
foreach ($automation->getSteps() as $id => $step) {
|
||||
if ((string)$id !== $step->getId()) {
|
||||
// translators: %1$s is the ID of the step, %2$s is its index in the steps object.
|
||||
throw Exceptions::automationStructureNotValid(
|
||||
sprintf(__("Step with ID '%1\$s' stored under a mismatched index '%2\$s'.", 'mailpoet'), $step->getId(), $id),
|
||||
self::RULE_ID
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class NoCycleRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'no-cycle';
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$step = $node->getStep();
|
||||
$parents = $node->getParents();
|
||||
$parentIdsMap = array_combine(
|
||||
array_map(function (Step $parent) {
|
||||
return $parent->getId();
|
||||
}, $node->getParents()),
|
||||
$parents
|
||||
) ?: [];
|
||||
|
||||
foreach ($step->getNextStepIds() as $nextStepId) {
|
||||
if ($nextStepId === $step->getId() || isset($parentIdsMap[$nextStepId])) {
|
||||
throw Exceptions::automationStructureNotValid(__('Cycle found in automation graph', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class NoDuplicateEdgesRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'no-duplicate-edges';
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$visitedNextStepIdsMap = [];
|
||||
foreach ($node->getStep()->getNextStepIds() as $nextStepId) {
|
||||
if (isset($visitedNextStepIdsMap[$nextStepId])) {
|
||||
throw Exceptions::automationStructureNotValid(__('Duplicate next step definition found', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
$visitedNextStepIdsMap[$nextStepId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class NoJoinRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'no-join';
|
||||
|
||||
/** @var array<string|int, Step[]> */
|
||||
private $directParentMap = [];
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
$this->directParentMap = [];
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$step = $node->getStep();
|
||||
foreach ($step->getNextStepIds() as $nextStepId) {
|
||||
$this->directParentMap[$nextStepId] = array_merge($this->directParentMap[$nextStepId] ?? [], [$step]);
|
||||
}
|
||||
|
||||
if (count($this->directParentMap[$step->getId()] ?? []) > 1) {
|
||||
throw Exceptions::automationStructureNotValid(__('Path join found in automation graph', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class NoUnreachableStepsRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'no-unreachable-steps';
|
||||
|
||||
/** @var AutomationNode[] */
|
||||
private $visitedNodes = [];
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
$this->visitedNodes = [];
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$this->visitedNodes[$node->getStep()->getId()] = $node;
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
if (count($this->visitedNodes) !== count($automation->getSteps())) {
|
||||
throw Exceptions::automationStructureNotValid(__('Unreachable steps found in automation graph', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class TriggerNeedsToBeFollowedByActionRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'trigger-needs-to-be-followed-by-action';
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
if (!$automation->needsFullValidation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$step = $node->getStep();
|
||||
if ($step->getType() !== Step::TYPE_TRIGGER) {
|
||||
return;
|
||||
}
|
||||
|
||||
$nextStepIds = $step->getNextStepIds();
|
||||
if (!count($nextStepIds)) {
|
||||
throw Exceptions::automationStructureNotValid(__('A trigger needs to be followed by an action.', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
|
||||
foreach ($nextStepIds as $nextStepsId) {
|
||||
$step = $automation->getStep($nextStepsId);
|
||||
if ($step && $step->getType() === Step::TYPE_ACTION) {
|
||||
continue;
|
||||
}
|
||||
throw Exceptions::automationStructureNotValid(__('A trigger needs to be followed by an action.', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class TriggersUnderRootRule implements AutomationNodeVisitor {
|
||||
public const RULE_ID = 'triggers-under-root';
|
||||
|
||||
/** @var array<string, Step> $triggersMap */
|
||||
private $triggersMap = [];
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
$this->triggersMap = [];
|
||||
foreach ($automation->getSteps() as $step) {
|
||||
if ($step->getType() === 'trigger') {
|
||||
$this->triggersMap[$step->getId()] = $step;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$step = $node->getStep();
|
||||
if ($step->getType() === Step::TYPE_ROOT) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($step->getNextStepIds() as $nextStepId) {
|
||||
if (isset($this->triggersMap[$nextStepId])) {
|
||||
throw Exceptions::automationStructureNotValid(__('Trigger must be a direct descendant of automation root', 'mailpoet'), self::RULE_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Exceptions\InvalidStateException;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationStorage;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class UnknownStepRule implements AutomationNodeVisitor {
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
/** @var AutomationStorage */
|
||||
private $automationStorage;
|
||||
|
||||
/** @var Automation|null|false */
|
||||
private $cachedExistingAutomation = false;
|
||||
|
||||
public function __construct(
|
||||
Registry $registry,
|
||||
AutomationStorage $automationStorage
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
$this->automationStorage = $automationStorage;
|
||||
}
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
$this->cachedExistingAutomation = false;
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$step = $node->getStep();
|
||||
$registryStep = $this->registry->getStep($step->getKey());
|
||||
|
||||
// step not registered (e.g. plugin was deactivated) - allow saving it only if it hasn't changed
|
||||
if (!$registryStep) {
|
||||
$currentAutomation = $this->getCurrentAutomation($automation);
|
||||
$currentStep = $currentAutomation ? ($currentAutomation->getSteps()[$step->getId()] ?? null) : null;
|
||||
if (!$currentStep || $step->toArray() !== $currentStep->toArray()) {
|
||||
throw Exceptions::automationStepModifiedWhenUnknown($step);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
|
||||
private function getCurrentAutomation(Automation $automation): ?Automation {
|
||||
try {
|
||||
$id = $automation->getId();
|
||||
if ($this->cachedExistingAutomation === false) {
|
||||
$this->cachedExistingAutomation = $this->automationStorage->getAutomation($id);
|
||||
}
|
||||
} catch (InvalidStateException $e) {
|
||||
// for new automations, no automation ID is set
|
||||
$this->cachedExistingAutomation = null;
|
||||
}
|
||||
return $this->cachedExistingAutomation;
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
use MailPoet\Validator\ValidationException;
|
||||
use MailPoet\Validator\Validator;
|
||||
|
||||
class ValidStepArgsRule implements AutomationNodeVisitor {
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
/** @var Validator */
|
||||
private $validator;
|
||||
|
||||
public function __construct(
|
||||
Registry $registry,
|
||||
Validator $validator
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$step = $node->getStep();
|
||||
$registryStep = $this->registry->getStep($step->getKey());
|
||||
if (!$registryStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema = $registryStep->getArgsSchema();
|
||||
$properties = $schema->toArray()['properties'] ?? null;
|
||||
if (!$properties) {
|
||||
$this->validator->validate($schema, $step->getArgs());
|
||||
return;
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
foreach ($properties as $property => $propertySchema) {
|
||||
$schemaToValidate = array_merge(
|
||||
$schema->toArray(),
|
||||
['properties' => [$property => $propertySchema]]
|
||||
);
|
||||
try {
|
||||
$this->validator->validateSchemaArray(
|
||||
$schemaToValidate,
|
||||
$step->getArgs(),
|
||||
$property
|
||||
);
|
||||
} catch (ValidationException $e) {
|
||||
$errors[$property] = $e->getWpError()->get_error_code();
|
||||
}
|
||||
}
|
||||
if ($errors) {
|
||||
$throwable = ValidationException::create();
|
||||
foreach ($errors as $errorKey => $errorMsg) {
|
||||
$throwable->withError((string)$errorKey, (string)$errorMsg);
|
||||
}
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Control\SubjectTransformerHandler;
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Filter;
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\Integration\Subject;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
use MailPoet\Validator\ValidationException;
|
||||
use MailPoet\Validator\Validator;
|
||||
|
||||
class ValidStepFiltersRule implements AutomationNodeVisitor {
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
/** @var SubjectTransformerHandler */
|
||||
private $subjectTransformerHandler;
|
||||
|
||||
/** @var Validator */
|
||||
private $validator;
|
||||
|
||||
public function __construct(
|
||||
Registry $registry,
|
||||
SubjectTransformerHandler $subjectTransformerHandler,
|
||||
Validator $validator
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
$this->subjectTransformerHandler = $subjectTransformerHandler;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$filters = $node->getStep()->getFilters();
|
||||
$groups = $filters ? $filters->getGroups() : [];
|
||||
$errors = [];
|
||||
foreach ($groups as $group) {
|
||||
foreach ($group->getFilters() as $filter) {
|
||||
$registryFilter = $this->registry->getFilter($filter->getFieldType());
|
||||
if (!$registryFilter) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$this->validator->validate($registryFilter->getArgsSchema($filter->getCondition()), $filter->getArgs());
|
||||
} catch (ValidationException $e) {
|
||||
$errors[$filter->getId()] = $e->getWpError()->get_error_code();
|
||||
continue;
|
||||
}
|
||||
|
||||
// ensure that the field is available with the provided subjects
|
||||
$subjectKeys = $this->subjectTransformerHandler->getSubjectKeysForAutomation($automation);
|
||||
$filterSubject = $this->getFilterSubject($filter);
|
||||
if (!$filterSubject) {
|
||||
$errors[$filter->getId()] = __('Field not found', 'mailpoet');
|
||||
} elseif (!in_array($filterSubject->getKey(), $subjectKeys, true)) {
|
||||
// translators: %s is the name of a subject (data structure) that provides the field
|
||||
$errors[$filter->getId()] = sprintf(__('A trigger that provides %s is required', 'mailpoet'), $filterSubject->getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($errors) {
|
||||
$throwable = ValidationException::create()->withMessage('invalid-automation-filters');
|
||||
foreach ($errors as $errorKey => $errorMsg) {
|
||||
$throwable->withError((string)$errorKey, (string)$errorMsg);
|
||||
}
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
|
||||
/** @return Subject<Payload> */
|
||||
private function getFilterSubject(Filter $filter): ?Subject {
|
||||
foreach ($this->registry->getSubjects() as $subject) {
|
||||
foreach ($subject->getFields() as $field) {
|
||||
if ($field->getKey() === $filter->getFieldKey()) {
|
||||
return $subject;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Control\SubjectTransformerHandler;
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class ValidStepOrderRule implements AutomationNodeVisitor {
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
/** @var SubjectTransformerHandler */
|
||||
private $subjectTransformerHandler;
|
||||
|
||||
public function __construct(
|
||||
Registry $registry,
|
||||
SubjectTransformerHandler $subjectTransformerHandler
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
$this->subjectTransformerHandler = $subjectTransformerHandler;
|
||||
}
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$step = $node->getStep();
|
||||
$registryStep = $this->registry->getStep($step->getKey());
|
||||
if (!$registryStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
// triggers don't require any subjects (they provide them)
|
||||
if ($step->getType() === Step::TYPE_TRIGGER) {
|
||||
return;
|
||||
}
|
||||
|
||||
$requiredSubjectKeys = $registryStep->getSubjectKeys();
|
||||
if (!$requiredSubjectKeys) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subjectKeys = $this->subjectTransformerHandler->getSubjectKeysForAutomation($automation);
|
||||
$missingSubjectKeys = array_diff($requiredSubjectKeys, $subjectKeys);
|
||||
if (count($missingSubjectKeys) > 0) {
|
||||
throw Exceptions::missingRequiredSubjects($step, $missingSubjectKeys);
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Exceptions\UnexpectedValueException;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
use MailPoet\Validator\ValidationException;
|
||||
use Throwable;
|
||||
|
||||
class ValidStepRule implements AutomationNodeVisitor {
|
||||
/** @var AutomationNodeVisitor[] */
|
||||
private $rules;
|
||||
|
||||
/** @var array<string, array{step_id: string, fields: array<string,string>}> */
|
||||
private $errors = [];
|
||||
|
||||
/** @param AutomationNodeVisitor[] $rules */
|
||||
public function __construct(
|
||||
array $rules
|
||||
) {
|
||||
$this->rules = $rules;
|
||||
}
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
if (!$automation->needsFullValidation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->rules as $rule) {
|
||||
$rule->initialize($automation);
|
||||
}
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
if (!$automation->needsFullValidation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->rules as $rule) {
|
||||
$stepId = $node->getStep()->getId();
|
||||
try {
|
||||
$rule->visitNode($automation, $node);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
if (!isset($this->errors[$stepId])) {
|
||||
$this->errors[$stepId] = ['step_id' => $stepId, 'message' => $e->getMessage(), 'fields' => [], 'filters' => []];
|
||||
}
|
||||
$this->errors[$stepId]['fields'] = array_merge(
|
||||
$this->mapErrorCodesToErrorMessages($e->getErrors()),
|
||||
$this->errors[$stepId]['fields']
|
||||
);
|
||||
} catch (ValidationException $e) {
|
||||
if (!isset($this->errors[$stepId])) {
|
||||
$this->errors[$stepId] = ['step_id' => $stepId, 'message' => $e->getMessage(), 'fields' => [], 'filters' => []];
|
||||
}
|
||||
|
||||
$key = $rule instanceof ValidStepFiltersRule ? 'filters' : 'fields';
|
||||
/** @phpstan-ignore-next-line - PHPStan detects inconsistency in merged array */
|
||||
$this->errors[$stepId][$key] = array_merge(
|
||||
$this->mapErrorCodesToErrorMessages($e->getErrors()),
|
||||
$this->errors[$stepId][$key]
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
if (!isset($this->errors[$stepId])) {
|
||||
$this->errors[$stepId] = ['step_id' => $stepId, 'message' => __('Unknown error.', 'mailpoet'), 'fields' => [], 'filters' => []];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function mapErrorCodesToErrorMessages(array $errorCodes): array {
|
||||
|
||||
return array_map(
|
||||
function(string $errorCode): string {
|
||||
switch ($errorCode) {
|
||||
case "rest_property_required":
|
||||
return __('This is a required field.', 'mailpoet');
|
||||
case "rest_additional_properties_forbidden":
|
||||
case "rest_too_few_properties":
|
||||
case "rest_too_many_properties":
|
||||
return "";
|
||||
case "rest_invalid_type":
|
||||
case "rest_invalid_multiple":
|
||||
case "rest_not_in_enum":
|
||||
return __('This field is not well formed.', 'mailpoet');
|
||||
case "rest_too_few_items":
|
||||
return __('Please add more items.', 'mailpoet');
|
||||
case "rest_too_many_items":
|
||||
return __('Please remove some items.', 'mailpoet');
|
||||
case "rest_duplicate_items":
|
||||
return __('Please remove duplicate items.', 'mailpoet');
|
||||
case "rest_out_of_bounds":
|
||||
return __('This value is out of bounds.', 'mailpoet');
|
||||
case "rest_too_short":
|
||||
return __('This value is not long enough.', 'mailpoet');
|
||||
case "rest_too_long":
|
||||
return __('This value is too long.', 'mailpoet');
|
||||
case "rest_invalid_pattern":
|
||||
return __('This value is not well formed.', 'mailpoet');
|
||||
case "rest_no_matching_schema":
|
||||
return __('This value does not match the expected format.', 'mailpoet');
|
||||
case "rest_one_of_multiple_matches":
|
||||
return __('This value is not matching the correct times.', 'mailpoet');
|
||||
case "rest_invalid_hex_color":
|
||||
return __('This value is not a hex formatted color.', 'mailpoet');
|
||||
case "rest_invalid_date":
|
||||
return __('This value is not a date.', 'mailpoet');
|
||||
case "rest_invalid_email":
|
||||
return __('This value is not an email address.', 'mailpoet');
|
||||
case "rest_invalid_ip":
|
||||
return __('This value is not an IP address.', 'mailpoet');
|
||||
case "rest_invalid_uuid":
|
||||
return __('This value is not an UUID.', 'mailpoet');
|
||||
default:
|
||||
return $errorCode;
|
||||
}
|
||||
},
|
||||
$errorCodes
|
||||
);
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
if (!$automation->needsFullValidation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->rules as $rule) {
|
||||
$rule->complete($automation);
|
||||
}
|
||||
|
||||
if ($this->errors) {
|
||||
throw Exceptions::automationNotValid(__('Some steps are not valid', 'mailpoet'), $this->errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation\AutomationRules;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\Step;
|
||||
use MailPoet\Automation\Engine\Data\StepValidationArgs;
|
||||
use MailPoet\Automation\Engine\Exceptions;
|
||||
use MailPoet\Automation\Engine\Integration\Payload;
|
||||
use MailPoet\Automation\Engine\Integration\Subject;
|
||||
use MailPoet\Automation\Engine\Registry;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNode;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationNodeVisitor;
|
||||
|
||||
class ValidStepValidationRule implements AutomationNodeVisitor {
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
public function __construct(
|
||||
Registry $registry
|
||||
) {
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
public function initialize(Automation $automation): void {
|
||||
}
|
||||
|
||||
public function visitNode(Automation $automation, AutomationNode $node): void {
|
||||
$step = $node->getStep();
|
||||
$registryStep = $this->registry->getStep($step->getKey());
|
||||
if (!$registryStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subjects = $this->collectSubjects($automation, $node->getParents());
|
||||
$args = new StepValidationArgs($automation, $step, $subjects);
|
||||
$registryStep->validate($args);
|
||||
}
|
||||
|
||||
public function complete(Automation $automation): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Step[] $parents
|
||||
* @return Subject<Payload>[]
|
||||
*/
|
||||
private function collectSubjects(Automation $automation, array $parents): array {
|
||||
$triggers = array_filter($parents, function (Step $step) {
|
||||
return $step->getType() === Step::TYPE_TRIGGER;
|
||||
});
|
||||
|
||||
$subjectKeys = [];
|
||||
foreach ($triggers as $trigger) {
|
||||
$registryTrigger = $this->registry->getTrigger($trigger->getKey());
|
||||
if (!$registryTrigger) {
|
||||
throw Exceptions::automationTriggerNotFound($automation->getId(), $trigger->getKey());
|
||||
}
|
||||
$subjectKeys = array_merge($subjectKeys, $registryTrigger->getSubjectKeys());
|
||||
}
|
||||
|
||||
$subjects = [];
|
||||
foreach (array_unique($subjectKeys) as $key) {
|
||||
$subject = $this->registry->getSubject($key);
|
||||
if (!$subject) {
|
||||
throw Exceptions::subjectNotFound($key);
|
||||
}
|
||||
$subjects[] = $subject;
|
||||
}
|
||||
return $subjects;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Validator\Builder;
|
||||
use MailPoet\Validator\Schema\ArraySchema;
|
||||
use MailPoet\Validator\Schema\ObjectSchema;
|
||||
|
||||
class AutomationSchema {
|
||||
public static function getSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'id' => Builder::integer()->required(),
|
||||
'name' => Builder::string()->minLength(1)->required(),
|
||||
'status' => Builder::string()->required(),
|
||||
'steps' => self::getStepsSchema()->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getStepsSchema(): ObjectSchema {
|
||||
return Builder::object()
|
||||
->properties(['root' => self::getRootStepSchema()->required()])
|
||||
->additionalProperties(self::getStepSchema());
|
||||
}
|
||||
|
||||
public static function getStepSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'id' => Builder::string()->required(),
|
||||
'type' => Builder::string()->required(),
|
||||
'key' => Builder::string()->required(),
|
||||
'args' => Builder::object()->required(),
|
||||
'next_steps' => self::getNextStepsSchema()->required(),
|
||||
'filters' => self::getFiltersSchema()->nullable()->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRootStepSchema(): ObjectSchema {
|
||||
return Builder::object([
|
||||
'id' => Builder::string()->pattern('^root$'),
|
||||
'type' => Builder::string()->pattern('^root$'),
|
||||
'key' => Builder::string()->pattern('^core:root$'),
|
||||
'args' => Builder::object()->disableAdditionalProperties(),
|
||||
'next_steps' => self::getNextStepsSchema()->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getNextStepsSchema(): ArraySchema {
|
||||
return Builder::array(
|
||||
Builder::object([
|
||||
'id' => Builder::string()->required()->nullable(),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
public static function getFiltersSchema(): ObjectSchema {
|
||||
$operatorSchema = Builder::string()->pattern('^and|or$')->required();
|
||||
|
||||
$filterSchema = Builder::object([
|
||||
'id' => Builder::string()->required(),
|
||||
'field_type' => Builder::string()->required(),
|
||||
'field_key' => Builder::string()->required(),
|
||||
'condition' => Builder::string()->required(),
|
||||
'args' => Builder::object()->required(),
|
||||
]);
|
||||
|
||||
$filterGroupSchema = Builder::object([
|
||||
'id' => Builder::string()->required(),
|
||||
'operator' => $operatorSchema,
|
||||
'filters' => Builder::array($filterSchema)->minItems(1)->required(),
|
||||
]);
|
||||
|
||||
return Builder::object([
|
||||
'operator' => $operatorSchema,
|
||||
'groups' => Builder::array($filterGroupSchema)->minItems(1)->required(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Engine\Validation;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationGraph\AutomationWalker;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationRules\AtLeastOneTriggerRule;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationRules\ConsistentStepMapRule;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationRules\NoCycleRule;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationRules\NoDuplicateEdgesRule;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationRules\NoJoinRule;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationRules\NoUnreachableStepsRule;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationRules\TriggerNeedsToBeFollowedByActionRule;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationRules\TriggersUnderRootRule;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationRules\UnknownStepRule;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationRules\ValidStepArgsRule;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationRules\ValidStepFiltersRule;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationRules\ValidStepOrderRule;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationRules\ValidStepRule;
|
||||
use MailPoet\Automation\Engine\Validation\AutomationRules\ValidStepValidationRule;
|
||||
|
||||
class AutomationValidator {
|
||||
/** @var AutomationWalker */
|
||||
private $automationWalker;
|
||||
|
||||
/** @var ValidStepArgsRule */
|
||||
private $validStepArgsRule;
|
||||
|
||||
/** @var ValidStepFiltersRule */
|
||||
private $validStepFiltersRule;
|
||||
|
||||
/** @var ValidStepOrderRule */
|
||||
private $validStepOrderRule;
|
||||
|
||||
/** @var ValidStepValidationRule */
|
||||
private $validStepValidationRule;
|
||||
|
||||
/** @var UnknownStepRule */
|
||||
private $unknownStepRule;
|
||||
|
||||
public function __construct(
|
||||
UnknownStepRule $unknownStepRule,
|
||||
ValidStepArgsRule $validStepArgsRule,
|
||||
ValidStepFiltersRule $validStepFiltersRule,
|
||||
ValidStepOrderRule $validStepOrderRule,
|
||||
ValidStepValidationRule $validStepValidationRule,
|
||||
AutomationWalker $automationWalker
|
||||
) {
|
||||
$this->unknownStepRule = $unknownStepRule;
|
||||
$this->validStepArgsRule = $validStepArgsRule;
|
||||
$this->validStepFiltersRule = $validStepFiltersRule;
|
||||
$this->validStepOrderRule = $validStepOrderRule;
|
||||
$this->validStepValidationRule = $validStepValidationRule;
|
||||
$this->automationWalker = $automationWalker;
|
||||
}
|
||||
|
||||
public function validate(Automation $automation): void {
|
||||
$this->automationWalker->walk($automation, [
|
||||
new NoUnreachableStepsRule(),
|
||||
new ConsistentStepMapRule(),
|
||||
new NoDuplicateEdgesRule(),
|
||||
new TriggersUnderRootRule(),
|
||||
new NoCycleRule(),
|
||||
new NoJoinRule(),
|
||||
$this->unknownStepRule,
|
||||
new AtLeastOneTriggerRule(),
|
||||
new TriggerNeedsToBeFollowedByActionRule(),
|
||||
new ValidStepRule([
|
||||
$this->validStepArgsRule,
|
||||
$this->validStepFiltersRule,
|
||||
$this->validStepOrderRule,
|
||||
$this->validStepValidationRule,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
Reference in New Issue
Block a user