This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -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;
}
}
@@ -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;
}
@@ -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,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);
}
}
@@ -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 {
}
}
@@ -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 {
}
}
@@ -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 {
}
}
@@ -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 {
}
}
@@ -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);
}
}
}
@@ -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 {
}
}
@@ -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 {
}
}
@@ -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;
}
}
@@ -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 {
}
}
@@ -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;
}
}
@@ -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 {
}
}
@@ -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);
}
}
}
@@ -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,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