From ba35ddf6e6d836f4eff4059e347c8c7f47f2672f Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Fri, 9 Sep 2022 15:19:46 +0200 Subject: [PATCH] Extract automation API to MailPoet REST API [MAILPOET-4523] --- mailpoet/lib/API/REST/API.php | 102 ++++++++++++++++++ mailpoet/lib/API/REST/Endpoint.php | 18 ++++ .../API => API/REST}/EndpointContainer.php | 2 +- .../Engine/API => API/REST}/ErrorResponse.php | 2 +- mailpoet/lib/API/REST/Exception.php | 11 ++ .../Engine/API => API/REST}/Request.php | 2 +- .../Engine/API => API/REST}/Response.php | 2 +- mailpoet/lib/Automation/Engine/API/API.php | 75 +++---------- .../lib/Automation/Engine/API/Endpoint.php | 13 +-- .../System/DatabaseDeleteEndpoint.php | 4 +- .../Endpoints/System/DatabasePostEndpoint.php | 4 +- .../WorkflowTemplatesGetEndpoint.php | 4 +- .../WorkflowsCreateFromTemplateEndpoint.php | 4 +- .../Workflows/WorkflowsGetEndpoint.php | 4 +- .../Workflows/WorkflowsPutEndpoint.php | 4 +- .../Engine/Exceptions/Exception.php | 4 +- mailpoet/lib/Config/Initializer.php | 7 ++ mailpoet/lib/DI/ContainerConfigurator.php | 8 +- mailpoet/lib/WP/Functions.php | 4 + .../REST/{Automation => }/API/Endpoint.php | 6 +- .../{Automation => }/API/EndpointTest.php | 14 +-- 21 files changed, 190 insertions(+), 104 deletions(-) create mode 100644 mailpoet/lib/API/REST/API.php create mode 100644 mailpoet/lib/API/REST/Endpoint.php rename mailpoet/lib/{Automation/Engine/API => API/REST}/EndpointContainer.php (93%) rename mailpoet/lib/{Automation/Engine/API => API/REST}/ErrorResponse.php (88%) create mode 100644 mailpoet/lib/API/REST/Exception.php rename mailpoet/lib/{Automation/Engine/API => API/REST}/Request.php (92%) rename mailpoet/lib/{Automation/Engine/API => API/REST}/Response.php (85%) rename mailpoet/tests/integration/REST/{Automation => }/API/Endpoint.php (87%) rename mailpoet/tests/integration/REST/{Automation => }/API/EndpointTest.php (91%) diff --git a/mailpoet/lib/API/REST/API.php b/mailpoet/lib/API/REST/API.php new file mode 100644 index 0000000000..284efcda1f --- /dev/null +++ b/mailpoet/lib/API/REST/API.php @@ -0,0 +1,102 @@ +endpointContainer = $endpointContainer; + $this->wp = $wp; + } + + public function init(): void { + $this->wp->addAction(self::WP_REST_API_INIT_ACTION, function () { + $this->wp->doAction(self::REST_API_INIT_ACTION, [$this]); + }); + } + + public function registerGetRoute(string $route, string $endpoint): void { + $this->registerRoute($route, $endpoint, 'GET'); + } + + public function registerPostRoute(string $route, string $endpoint): void { + $this->registerRoute($route, $endpoint, 'POST'); + } + + public function registerPutRoute(string $route, string $endpoint): void { + $this->registerRoute($route, $endpoint, 'PUT'); + } + + public function registerPatchRoute(string $route, string $endpoint): void { + $this->registerRoute($route, $endpoint, 'PATCH'); + } + + public function registerDeleteRoute(string $route, string $endpoint): void { + $this->registerRoute($route, $endpoint, 'DELETE'); + } + + protected function registerRoute(string $route, string $endpointClass, string $method): void { + $schema = array_map(function (Schema $field) { + return $field->toArray(); + }, $endpointClass::getRequestSchema()); + + $this->wp->registerRestRoute(self::PREFIX, $route, [ + 'methods' => $method, + 'callback' => function (WP_REST_Request $wpRequest) use ($endpointClass, $schema) { + try { + $endpoint = $this->endpointContainer->get($endpointClass); + $wpRequest = $this->sanitizeUnknownParams($wpRequest, $schema); + $request = new Request($wpRequest); + return $endpoint->handle($request); + } catch (Throwable $e) { + return $this->convertToErrorResponse($e); + } + }, + 'permission_callback' => function () use ($endpointClass) { + $endpoint = $this->endpointContainer->get($endpointClass); + return $endpoint->checkPermissions(); + }, + 'args' => $schema, + ]); + } + + private function convertToErrorResponse(Throwable $e): ErrorResponse { + $response = $e instanceof Exception + ? new ErrorResponse($e->getStatusCode(), $e->getMessage(), $e->getErrorCode()) + : new ErrorResponse(500, __('An unknown error occurred.', 'mailpoet'), 'mailpoet_automation_unknown_error'); + + if ($response->get_status() >= 500) { + error_log((string)$e); // phpcs:ignore Squiz.PHP.DiscouragedFunctions + } + return $response; + } + + private function sanitizeUnknownParams(WP_REST_Request $wpRequest, array $args): WP_REST_Request { + // Remove all params that are not declared in the schema, so we use just the validated ones. + // Note that this doesn't work recursively for object properties as it is harder to solve + // with features like oneOf, anyOf, additional properties, or pattern properties. + $extraParams = array_diff(array_keys($wpRequest->get_params()), array_keys($args)); + foreach ($extraParams as $extraParam) { + unset($wpRequest[(string)$extraParam]); + } + return $wpRequest; + } +} diff --git a/mailpoet/lib/API/REST/Endpoint.php b/mailpoet/lib/API/REST/Endpoint.php new file mode 100644 index 0000000000..df124179a8 --- /dev/null +++ b/mailpoet/lib/API/REST/Endpoint.php @@ -0,0 +1,18 @@ + */ + public static function getRequestSchema(): array { + return []; + } +} diff --git a/mailpoet/lib/Automation/Engine/API/EndpointContainer.php b/mailpoet/lib/API/REST/EndpointContainer.php similarity index 93% rename from mailpoet/lib/Automation/Engine/API/EndpointContainer.php rename to mailpoet/lib/API/REST/EndpointContainer.php index e9299e46ec..7c4d55ba60 100644 --- a/mailpoet/lib/Automation/Engine/API/EndpointContainer.php +++ b/mailpoet/lib/API/REST/EndpointContainer.php @@ -1,6 +1,6 @@ endpointContainer = $endpointContainer; + $this->api = $api; $this->wordPress = $wordPress; } public function initialize(): void { - $this->wordPress->addAction(self::WP_REST_API_INIT_ACTION, function () { + $this->wordPress->addAction(MailPoetApi::REST_API_INIT_ACTION, function () { $this->wordPress->doAction(Hooks::API_INITIALIZE, [$this]); }); } public function registerGetRoute(string $route, string $endpoint): void { - $this->registerRoute($route, $endpoint, 'GET'); + $this->api->registerGetRoute(self::PREFIX . $route, $endpoint); } public function registerPostRoute(string $route, string $endpoint): void { - $this->registerRoute($route, $endpoint, 'POST'); + $this->api->registerPostRoute(self::PREFIX . $route, $endpoint); } public function registerPutRoute(string $route, string $endpoint): void { - $this->registerRoute($route, $endpoint, 'PUT'); + $this->api->registerPutRoute(self::PREFIX . $route, $endpoint); } public function registerPatchRoute(string $route, string $endpoint): void { - $this->registerRoute($route, $endpoint, 'PATCH'); + $this->api->registerPatchRoute(self::PREFIX . $route, $endpoint); } public function registerDeleteRoute(string $route, string $endpoint): void { - $this->registerRoute($route, $endpoint, 'DELETE'); - } - - private function registerRoute(string $route, string $endpointClass, string $method): void { - $schema = array_map(function (Schema $field) { - return $field->toArray(); - }, $endpointClass::getRequestSchema()); - - $this->wordPress->registerRestRoute(self::PREFIX, $route, [ - 'methods' => $method, - 'callback' => function (WP_REST_Request $wpRequest) use ($endpointClass, $schema) { - try { - $endpoint = $this->endpointContainer->get($endpointClass); - $wpRequest = $this->sanitizeUnknownParams($wpRequest, $schema); - $request = new Request($wpRequest); - return $endpoint->handle($request); - } catch (Throwable $e) { - return $this->convertToErrorResponse($e); - } - }, - 'permission_callback' => function () use ($endpointClass) { - $endpoint = $this->endpointContainer->get($endpointClass); - return $endpoint->checkPermissions(); - }, - 'args' => $schema, - ]); - } - - private function convertToErrorResponse(Throwable $e): ErrorResponse { - $response = $e instanceof Exception - ? new ErrorResponse($e->getStatusCode(), $e->getMessage(), $e->getErrorCode()) - : new ErrorResponse(500, __('An unknown error occurred.', 'mailpoet'), 'mailpoet_automation_unknown_error'); - - if ($response->get_status() >= 500) { - error_log((string)$e); // phpcs:ignore Squiz.PHP.DiscouragedFunctions - } - return $response; - } - - private function sanitizeUnknownParams(WP_REST_Request $wpRequest, array $args): WP_REST_Request { - // Remove all params that are not declared in the schema, so we use just the validated ones. - // Note that this doesn't work recursively for object properties as it is harder to solve - // with features like oneOf, anyOf, additional properties, or pattern properties. - $extraParams = array_diff(array_keys($wpRequest->get_params()), array_keys($args)); - foreach ($extraParams as $extraParam) { - unset($wpRequest[(string)$extraParam]); - } - return $wpRequest; + $this->api->registerDeleteRoute(self::PREFIX . $route, $endpoint); } } diff --git a/mailpoet/lib/Automation/Engine/API/Endpoint.php b/mailpoet/lib/Automation/Engine/API/Endpoint.php index 21754e19b6..8f7d9c9350 100644 --- a/mailpoet/lib/Automation/Engine/API/Endpoint.php +++ b/mailpoet/lib/Automation/Engine/API/Endpoint.php @@ -2,20 +2,11 @@ namespace MailPoet\Automation\Engine\API; +use MailPoet\API\REST\Endpoint as MailPoetEndpoint; use MailPoet\Automation\Engine\Engine; -use MailPoet\Validator\Schema; - -use function current_user_can; - -abstract class Endpoint { - abstract public function handle(Request $request): Response; +abstract class Endpoint extends MailPoetEndpoint { public function checkPermissions(): bool { return current_user_can(Engine::CAPABILITY_MANAGE_AUTOMATIONS); } - - /** @return array */ - public static function getRequestSchema(): array { - return []; - } } diff --git a/mailpoet/lib/Automation/Engine/Endpoints/System/DatabaseDeleteEndpoint.php b/mailpoet/lib/Automation/Engine/Endpoints/System/DatabaseDeleteEndpoint.php index 826c13c776..9d0ef86a63 100644 --- a/mailpoet/lib/Automation/Engine/Endpoints/System/DatabaseDeleteEndpoint.php +++ b/mailpoet/lib/Automation/Engine/Endpoints/System/DatabaseDeleteEndpoint.php @@ -2,9 +2,9 @@ namespace MailPoet\Automation\Engine\Endpoints\System; +use MailPoet\API\REST\Request; +use MailPoet\API\REST\Response; use MailPoet\Automation\Engine\API\Endpoint; -use MailPoet\Automation\Engine\API\Request; -use MailPoet\Automation\Engine\API\Response; use MailPoet\Automation\Engine\Migrations\Migrator; use MailPoet\Features\FeatureFlagsController; use MailPoet\Features\FeaturesController; diff --git a/mailpoet/lib/Automation/Engine/Endpoints/System/DatabasePostEndpoint.php b/mailpoet/lib/Automation/Engine/Endpoints/System/DatabasePostEndpoint.php index 958c11ed8a..e6d1ec883e 100644 --- a/mailpoet/lib/Automation/Engine/Endpoints/System/DatabasePostEndpoint.php +++ b/mailpoet/lib/Automation/Engine/Endpoints/System/DatabasePostEndpoint.php @@ -2,9 +2,9 @@ namespace MailPoet\Automation\Engine\Endpoints\System; +use MailPoet\API\REST\Request; +use MailPoet\API\REST\Response; use MailPoet\Automation\Engine\API\Endpoint; -use MailPoet\Automation\Engine\API\Request; -use MailPoet\Automation\Engine\API\Response; use MailPoet\Automation\Engine\Migrations\Migrator; class DatabasePostEndpoint extends Endpoint { diff --git a/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowTemplatesGetEndpoint.php b/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowTemplatesGetEndpoint.php index 81cda63575..8c0819275f 100644 --- a/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowTemplatesGetEndpoint.php +++ b/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowTemplatesGetEndpoint.php @@ -2,9 +2,9 @@ namespace MailPoet\Automation\Engine\Endpoints\Workflows; +use MailPoet\API\REST\Request; +use MailPoet\API\REST\Response; use MailPoet\Automation\Engine\API\Endpoint; -use MailPoet\Automation\Engine\API\Request; -use MailPoet\Automation\Engine\API\Response; use MailPoet\Automation\Engine\Data\WorkflowTemplate; use MailPoet\Automation\Engine\Storage\WorkflowTemplateStorage; use MailPoet\Validator\Builder; diff --git a/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowsCreateFromTemplateEndpoint.php b/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowsCreateFromTemplateEndpoint.php index ff6bdd2304..8abc7791df 100644 --- a/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowsCreateFromTemplateEndpoint.php +++ b/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowsCreateFromTemplateEndpoint.php @@ -2,9 +2,9 @@ namespace MailPoet\Automation\Engine\Endpoints\Workflows; +use MailPoet\API\REST\Request; +use MailPoet\API\REST\Response; use MailPoet\Automation\Engine\API\Endpoint; -use MailPoet\Automation\Engine\API\Request; -use MailPoet\Automation\Engine\API\Response; use MailPoet\Automation\Engine\Builder\CreateWorkflowFromTemplateController; use MailPoet\Validator\Builder; diff --git a/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowsGetEndpoint.php b/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowsGetEndpoint.php index 2b9a5f1c26..43a47a267b 100644 --- a/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowsGetEndpoint.php +++ b/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowsGetEndpoint.php @@ -3,9 +3,9 @@ namespace MailPoet\Automation\Engine\Endpoints\Workflows; use DateTimeImmutable; +use MailPoet\API\REST\Request; +use MailPoet\API\REST\Response; use MailPoet\Automation\Engine\API\Endpoint; -use MailPoet\Automation\Engine\API\Request; -use MailPoet\Automation\Engine\API\Response; use MailPoet\Automation\Engine\Data\Workflow; use MailPoet\Automation\Engine\Storage\WorkflowStorage; use MailPoet\Validator\Builder; diff --git a/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowsPutEndpoint.php b/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowsPutEndpoint.php index eaa2e956c7..08aa74c9ae 100644 --- a/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowsPutEndpoint.php +++ b/mailpoet/lib/Automation/Engine/Endpoints/Workflows/WorkflowsPutEndpoint.php @@ -3,9 +3,9 @@ namespace MailPoet\Automation\Engine\Endpoints\Workflows; use DateTimeImmutable; +use MailPoet\API\REST\Request; +use MailPoet\API\REST\Response; use MailPoet\Automation\Engine\API\Endpoint; -use MailPoet\Automation\Engine\API\Request; -use MailPoet\Automation\Engine\API\Response; use MailPoet\Automation\Engine\Builder\UpdateWorkflowController; use MailPoet\Automation\Engine\Data\NextStep; use MailPoet\Automation\Engine\Data\Step; diff --git a/mailpoet/lib/Automation/Engine/Exceptions/Exception.php b/mailpoet/lib/Automation/Engine/Exceptions/Exception.php index 7a5790ff3e..203339c513 100644 --- a/mailpoet/lib/Automation/Engine/Exceptions/Exception.php +++ b/mailpoet/lib/Automation/Engine/Exceptions/Exception.php @@ -2,12 +2,14 @@ namespace MailPoet\Automation\Engine\Exceptions; +use Exception as PhpException; +use MailPoet\API\REST\Exception as RestException; use Throwable; /** * Frames all MailPoet Automation exceptions ("$e instanceof MailPoet\Automation\Exception"). */ -abstract class Exception extends \Exception { +abstract class Exception extends PhpException implements RestException { /** @var int */ protected $statusCode = 500; diff --git a/mailpoet/lib/Config/Initializer.php b/mailpoet/lib/Config/Initializer.php index 1b8c5050d1..2f5b5420f8 100644 --- a/mailpoet/lib/Config/Initializer.php +++ b/mailpoet/lib/Config/Initializer.php @@ -3,6 +3,7 @@ namespace MailPoet\Config; use MailPoet\API\JSON\API; +use MailPoet\API\REST\API as RestApi; use MailPoet\AutomaticEmails\AutomaticEmails; use MailPoet\Automation\Engine\Engine; use MailPoet\Automation\Engine\Hooks as AutomationHooks; @@ -37,6 +38,9 @@ class Initializer { /** @var API */ private $api; + /** @var RestApi */ + private $restApi; + /** @var Activator */ private $activator; @@ -115,6 +119,7 @@ class Initializer { RendererFactory $rendererFactory, AccessControl $accessControl, API $api, + RestApi $restApi, Activator $activator, SettingsController $settings, Router\Router $router, @@ -143,6 +148,7 @@ class Initializer { $this->rendererFactory = $rendererFactory; $this->accessControl = $accessControl; $this->api = $api; + $this->restApi = $restApi; $this->activator = $activator; $this->settings = $settings; $this->router = $router; @@ -383,6 +389,7 @@ class Initializer { if (!defined(self::INITIALIZED)) return; try { $this->api->init(); + $this->restApi->init(); $this->router->init(); $this->setupUserLocale(); } catch (\Exception $e) { diff --git a/mailpoet/lib/DI/ContainerConfigurator.php b/mailpoet/lib/DI/ContainerConfigurator.php index a45863c5cf..fc6f9fc113 100644 --- a/mailpoet/lib/DI/ContainerConfigurator.php +++ b/mailpoet/lib/DI/ContainerConfigurator.php @@ -94,6 +94,11 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\API\JSON\ResponseBuilders\SegmentsResponseBuilder::class)->setPublic(true); $container->autowire(\MailPoet\API\JSON\ResponseBuilders\DynamicSegmentsResponseBuilder::class)->setPublic(true); $container->autowire(\MailPoet\API\JSON\ResponseBuilders\ScheduledTaskSubscriberResponseBuilder::class)->setPublic(true); + // REST API + $container->autowire(\MailPoet\API\REST\API::class)->setPublic(true); + $container->autowire(\MailPoet\API\REST\EndpointContainer::class) + ->setPublic(true) + ->setArgument('$container', new Reference(ContainerWrapper::class)); // Automatic emails $container->autowire(\MailPoet\AutomaticEmails\AutomaticEmails::class)->setPublic(true); $container->autowire(\MailPoet\AutomaticEmails\AutomaticEmailFactory::class)->setPublic(true); @@ -105,9 +110,6 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\AutomaticEmails\WooCommerce\Events\PurchasedProduct::class)->setPublic(true); // Automation $container->autowire(\MailPoet\Automation\Engine\API\API::class)->setPublic(true); - $container->autowire(\MailPoet\Automation\Engine\API\EndpointContainer::class) - ->setPublic(true) - ->setArgument('$container', new Reference(ContainerWrapper::class)); $container->autowire(\MailPoet\Automation\Engine\Builder\CreateWorkflowFromTemplateController::class)->setPublic(true); $container->autowire(\MailPoet\Automation\Engine\Builder\UpdateStepsController::class)->setPublic(true); $container->autowire(\MailPoet\Automation\Engine\Builder\UpdateWorkflowController::class)->setPublic(true); diff --git a/mailpoet/lib/WP/Functions.php b/mailpoet/lib/WP/Functions.php index 090ecb0afd..caea3c5ebf 100644 --- a/mailpoet/lib/WP/Functions.php +++ b/mailpoet/lib/WP/Functions.php @@ -762,6 +762,10 @@ class Functions { return rest_url($path, $scheme); } + public function registerRestRoute(string $namespace, string $route, array $args = [], bool $override = false): bool { + return register_rest_route($namespace, $route, $args, $override); + } + /** * @param mixed $value * @return true|WP_Error diff --git a/mailpoet/tests/integration/REST/Automation/API/Endpoint.php b/mailpoet/tests/integration/REST/API/Endpoint.php similarity index 87% rename from mailpoet/tests/integration/REST/Automation/API/Endpoint.php rename to mailpoet/tests/integration/REST/API/Endpoint.php index 9b8dedf4ff..ca3a0c51fd 100644 --- a/mailpoet/tests/integration/REST/Automation/API/Endpoint.php +++ b/mailpoet/tests/integration/REST/API/Endpoint.php @@ -2,9 +2,9 @@ namespace MailPoet\REST\Automation\API\Endpoints; -use MailPoet\Automation\Engine\API\Endpoint as APIEndpoint; -use MailPoet\Automation\Engine\API\Request; -use MailPoet\Automation\Engine\API\Response; +use MailPoet\API\REST\Endpoint as APIEndpoint; +use MailPoet\API\REST\Request; +use MailPoet\API\REST\Response; use MailPoet\Validator\Builder; class Endpoint extends APIEndpoint { diff --git a/mailpoet/tests/integration/REST/Automation/API/EndpointTest.php b/mailpoet/tests/integration/REST/API/EndpointTest.php similarity index 91% rename from mailpoet/tests/integration/REST/Automation/API/EndpointTest.php rename to mailpoet/tests/integration/REST/API/EndpointTest.php index f65fb36168..ee3c8c36f4 100644 --- a/mailpoet/tests/integration/REST/Automation/API/EndpointTest.php +++ b/mailpoet/tests/integration/REST/API/EndpointTest.php @@ -2,19 +2,19 @@ namespace MailPoet\REST\Automation\API; -require_once __DIR__ . '/../../Test.php'; +require_once __DIR__ . '/../Test.php'; require_once __DIR__ . '/Endpoint.php'; -use MailPoet\Automation\Engine\API\API; -use MailPoet\Automation\Engine\API\EndpointContainer; -use MailPoet\Automation\Engine\API\Request; -use MailPoet\Automation\Engine\WordPress; +use MailPoet\API\REST\API; +use MailPoet\API\REST\EndpointContainer; +use MailPoet\API\REST\Request; use MailPoet\REST\Automation\API\Endpoints\Endpoint; use MailPoet\REST\Test; +use MailPoet\WP\Functions as WPFunctions; class EndpointTest extends Test { /** @var string */ - private $prefix = '/mailpoet/v1/automation/mailpoet-api-testing-route'; + private $prefix = '/mailpoet/v1/mailpoet-api-testing-route'; public function testGetParams(): void { $path = strtolower(__FUNCTION__); @@ -127,7 +127,7 @@ class EndpointTest extends Test { return new Endpoint($requestCallback); } ]), - $this->diContainer->get(WordPress::class) + $this->diContainer->get(WPFunctions::class) ); } }