Extract automation API to MailPoet REST API

[MAILPOET-4523]
This commit is contained in:
Jan Jakes
2022-09-09 15:19:46 +02:00
committed by John Oleksowicz
parent 1b5d6bd974
commit ba35ddf6e6
21 changed files with 190 additions and 104 deletions

View File

@ -0,0 +1,102 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\REST;
use MailPoet\Validator\Schema;
use MailPoet\WP\Functions as WPFunctions;
use Throwable;
use WP_REST_Request;
class API {
public const REST_API_INIT_ACTION = 'mailpoet/rest-api/init';
private const PREFIX = 'mailpoet/v1';
private const WP_REST_API_INIT_ACTION = 'rest_api_init';
/** @var EndpointContainer */
private $endpointContainer;
/** @var WPFunctions */
private $wp;
public function __construct(
EndpointContainer $endpointContainer,
WPFunctions $wp
) {
$this->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;
}
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\REST;
use MailPoet\Validator\Schema;
abstract class Endpoint {
abstract public function handle(Request $request): Response;
public function checkPermissions(): bool {
return current_user_can('admin');
}
/** @return array<string, Schema> */
public static function getRequestSchema(): array {
return [];
}
}

View File

@ -1,6 +1,6 @@
<?php declare(strict_types = 1); <?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\API; namespace MailPoet\API\REST;
use MailPoet\InvalidStateException; use MailPoet\InvalidStateException;
use MailPoetVendor\Psr\Container\ContainerInterface; use MailPoetVendor\Psr\Container\ContainerInterface;

View File

@ -1,6 +1,6 @@
<?php declare(strict_types = 1); <?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\API; namespace MailPoet\API\REST;
class ErrorResponse extends Response { class ErrorResponse extends Response {
public function __construct( public function __construct(

View File

@ -0,0 +1,11 @@
<?php declare(strict_types = 1);
namespace MailPoet\API\REST;
interface Exception {
public function getStatusCode();
public function getErrorCode(): string;
public function getErrors(): array;
}

View File

@ -1,6 +1,6 @@
<?php declare(strict_types = 1); <?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\API; namespace MailPoet\API\REST;
use WP_REST_Request; use WP_REST_Request;

View File

@ -1,6 +1,6 @@
<?php declare(strict_types = 1); <?php declare(strict_types = 1);
namespace MailPoet\Automation\Engine\API; namespace MailPoet\API\REST;
use WP_REST_Response; use WP_REST_Response;

View File

@ -2,101 +2,50 @@
namespace MailPoet\Automation\Engine\API; namespace MailPoet\Automation\Engine\API;
use MailPoet\Automation\Engine\Exceptions\Exception; use MailPoet\API\REST\API as MailPoetApi;
use MailPoet\Automation\Engine\Hooks; use MailPoet\Automation\Engine\Hooks;
use MailPoet\Automation\Engine\WordPress; use MailPoet\Automation\Engine\WordPress;
use MailPoet\Validator\Schema;
use Throwable;
use WP_REST_Request;
class API { class API {
private const PREFIX = 'mailpoet/v1/automation'; private const PREFIX = 'automation/';
private const WP_REST_API_INIT_ACTION = 'rest_api_init';
/** @var EndpointContainer */ /** @var MailPoetApi */
private $endpointContainer; private $api;
/** @var WordPress */ /** @var WordPress */
private $wordPress; private $wordPress;
public function __construct( public function __construct(
EndpointContainer $endpointContainer, MailPoetApi $api,
WordPress $wordPress WordPress $wordPress
) { ) {
$this->endpointContainer = $endpointContainer; $this->api = $api;
$this->wordPress = $wordPress; $this->wordPress = $wordPress;
} }
public function initialize(): void { 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]); $this->wordPress->doAction(Hooks::API_INITIALIZE, [$this]);
}); });
} }
public function registerGetRoute(string $route, string $endpoint): void { 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 { 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 { 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 { 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 { public function registerDeleteRoute(string $route, string $endpoint): void {
$this->registerRoute($route, $endpoint, 'DELETE'); $this->api->registerDeleteRoute(self::PREFIX . $route, $endpoint);
}
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;
} }
} }

View File

@ -2,20 +2,11 @@
namespace MailPoet\Automation\Engine\API; namespace MailPoet\Automation\Engine\API;
use MailPoet\API\REST\Endpoint as MailPoetEndpoint;
use MailPoet\Automation\Engine\Engine; 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 { public function checkPermissions(): bool {
return current_user_can(Engine::CAPABILITY_MANAGE_AUTOMATIONS); return current_user_can(Engine::CAPABILITY_MANAGE_AUTOMATIONS);
} }
/** @return array<string, Schema> */
public static function getRequestSchema(): array {
return [];
}
} }

View File

@ -2,9 +2,9 @@
namespace MailPoet\Automation\Engine\Endpoints\System; 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\Endpoint;
use MailPoet\Automation\Engine\API\Request;
use MailPoet\Automation\Engine\API\Response;
use MailPoet\Automation\Engine\Migrations\Migrator; use MailPoet\Automation\Engine\Migrations\Migrator;
use MailPoet\Features\FeatureFlagsController; use MailPoet\Features\FeatureFlagsController;
use MailPoet\Features\FeaturesController; use MailPoet\Features\FeaturesController;

View File

@ -2,9 +2,9 @@
namespace MailPoet\Automation\Engine\Endpoints\System; 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\Endpoint;
use MailPoet\Automation\Engine\API\Request;
use MailPoet\Automation\Engine\API\Response;
use MailPoet\Automation\Engine\Migrations\Migrator; use MailPoet\Automation\Engine\Migrations\Migrator;
class DatabasePostEndpoint extends Endpoint { class DatabasePostEndpoint extends Endpoint {

View File

@ -2,9 +2,9 @@
namespace MailPoet\Automation\Engine\Endpoints\Workflows; 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\Endpoint;
use MailPoet\Automation\Engine\API\Request;
use MailPoet\Automation\Engine\API\Response;
use MailPoet\Automation\Engine\Data\WorkflowTemplate; use MailPoet\Automation\Engine\Data\WorkflowTemplate;
use MailPoet\Automation\Engine\Storage\WorkflowTemplateStorage; use MailPoet\Automation\Engine\Storage\WorkflowTemplateStorage;
use MailPoet\Validator\Builder; use MailPoet\Validator\Builder;

View File

@ -2,9 +2,9 @@
namespace MailPoet\Automation\Engine\Endpoints\Workflows; 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\Endpoint;
use MailPoet\Automation\Engine\API\Request;
use MailPoet\Automation\Engine\API\Response;
use MailPoet\Automation\Engine\Builder\CreateWorkflowFromTemplateController; use MailPoet\Automation\Engine\Builder\CreateWorkflowFromTemplateController;
use MailPoet\Validator\Builder; use MailPoet\Validator\Builder;

View File

@ -3,9 +3,9 @@
namespace MailPoet\Automation\Engine\Endpoints\Workflows; namespace MailPoet\Automation\Engine\Endpoints\Workflows;
use DateTimeImmutable; use DateTimeImmutable;
use MailPoet\API\REST\Request;
use MailPoet\API\REST\Response;
use MailPoet\Automation\Engine\API\Endpoint; 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\Data\Workflow;
use MailPoet\Automation\Engine\Storage\WorkflowStorage; use MailPoet\Automation\Engine\Storage\WorkflowStorage;
use MailPoet\Validator\Builder; use MailPoet\Validator\Builder;

View File

@ -3,9 +3,9 @@
namespace MailPoet\Automation\Engine\Endpoints\Workflows; namespace MailPoet\Automation\Engine\Endpoints\Workflows;
use DateTimeImmutable; use DateTimeImmutable;
use MailPoet\API\REST\Request;
use MailPoet\API\REST\Response;
use MailPoet\Automation\Engine\API\Endpoint; 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\Builder\UpdateWorkflowController;
use MailPoet\Automation\Engine\Data\NextStep; use MailPoet\Automation\Engine\Data\NextStep;
use MailPoet\Automation\Engine\Data\Step; use MailPoet\Automation\Engine\Data\Step;

View File

@ -2,12 +2,14 @@
namespace MailPoet\Automation\Engine\Exceptions; namespace MailPoet\Automation\Engine\Exceptions;
use Exception as PhpException;
use MailPoet\API\REST\Exception as RestException;
use Throwable; use Throwable;
/** /**
* Frames all MailPoet Automation exceptions ("$e instanceof MailPoet\Automation\Exception"). * Frames all MailPoet Automation exceptions ("$e instanceof MailPoet\Automation\Exception").
*/ */
abstract class Exception extends \Exception { abstract class Exception extends PhpException implements RestException {
/** @var int */ /** @var int */
protected $statusCode = 500; protected $statusCode = 500;

View File

@ -3,6 +3,7 @@
namespace MailPoet\Config; namespace MailPoet\Config;
use MailPoet\API\JSON\API; use MailPoet\API\JSON\API;
use MailPoet\API\REST\API as RestApi;
use MailPoet\AutomaticEmails\AutomaticEmails; use MailPoet\AutomaticEmails\AutomaticEmails;
use MailPoet\Automation\Engine\Engine; use MailPoet\Automation\Engine\Engine;
use MailPoet\Automation\Engine\Hooks as AutomationHooks; use MailPoet\Automation\Engine\Hooks as AutomationHooks;
@ -37,6 +38,9 @@ class Initializer {
/** @var API */ /** @var API */
private $api; private $api;
/** @var RestApi */
private $restApi;
/** @var Activator */ /** @var Activator */
private $activator; private $activator;
@ -115,6 +119,7 @@ class Initializer {
RendererFactory $rendererFactory, RendererFactory $rendererFactory,
AccessControl $accessControl, AccessControl $accessControl,
API $api, API $api,
RestApi $restApi,
Activator $activator, Activator $activator,
SettingsController $settings, SettingsController $settings,
Router\Router $router, Router\Router $router,
@ -143,6 +148,7 @@ class Initializer {
$this->rendererFactory = $rendererFactory; $this->rendererFactory = $rendererFactory;
$this->accessControl = $accessControl; $this->accessControl = $accessControl;
$this->api = $api; $this->api = $api;
$this->restApi = $restApi;
$this->activator = $activator; $this->activator = $activator;
$this->settings = $settings; $this->settings = $settings;
$this->router = $router; $this->router = $router;
@ -383,6 +389,7 @@ class Initializer {
if (!defined(self::INITIALIZED)) return; if (!defined(self::INITIALIZED)) return;
try { try {
$this->api->init(); $this->api->init();
$this->restApi->init();
$this->router->init(); $this->router->init();
$this->setupUserLocale(); $this->setupUserLocale();
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@ -94,6 +94,11 @@ class ContainerConfigurator implements IContainerConfigurator {
$container->autowire(\MailPoet\API\JSON\ResponseBuilders\SegmentsResponseBuilder::class)->setPublic(true); $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\DynamicSegmentsResponseBuilder::class)->setPublic(true);
$container->autowire(\MailPoet\API\JSON\ResponseBuilders\ScheduledTaskSubscriberResponseBuilder::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 // Automatic emails
$container->autowire(\MailPoet\AutomaticEmails\AutomaticEmails::class)->setPublic(true); $container->autowire(\MailPoet\AutomaticEmails\AutomaticEmails::class)->setPublic(true);
$container->autowire(\MailPoet\AutomaticEmails\AutomaticEmailFactory::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); $container->autowire(\MailPoet\AutomaticEmails\WooCommerce\Events\PurchasedProduct::class)->setPublic(true);
// Automation // Automation
$container->autowire(\MailPoet\Automation\Engine\API\API::class)->setPublic(true); $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\CreateWorkflowFromTemplateController::class)->setPublic(true);
$container->autowire(\MailPoet\Automation\Engine\Builder\UpdateStepsController::class)->setPublic(true); $container->autowire(\MailPoet\Automation\Engine\Builder\UpdateStepsController::class)->setPublic(true);
$container->autowire(\MailPoet\Automation\Engine\Builder\UpdateWorkflowController::class)->setPublic(true); $container->autowire(\MailPoet\Automation\Engine\Builder\UpdateWorkflowController::class)->setPublic(true);

View File

@ -762,6 +762,10 @@ class Functions {
return rest_url($path, $scheme); 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 * @param mixed $value
* @return true|WP_Error * @return true|WP_Error

View File

@ -2,9 +2,9 @@
namespace MailPoet\REST\Automation\API\Endpoints; namespace MailPoet\REST\Automation\API\Endpoints;
use MailPoet\Automation\Engine\API\Endpoint as APIEndpoint; use MailPoet\API\REST\Endpoint as APIEndpoint;
use MailPoet\Automation\Engine\API\Request; use MailPoet\API\REST\Request;
use MailPoet\Automation\Engine\API\Response; use MailPoet\API\REST\Response;
use MailPoet\Validator\Builder; use MailPoet\Validator\Builder;
class Endpoint extends APIEndpoint { class Endpoint extends APIEndpoint {

View File

@ -2,19 +2,19 @@
namespace MailPoet\REST\Automation\API; namespace MailPoet\REST\Automation\API;
require_once __DIR__ . '/../../Test.php'; require_once __DIR__ . '/../Test.php';
require_once __DIR__ . '/Endpoint.php'; require_once __DIR__ . '/Endpoint.php';
use MailPoet\Automation\Engine\API\API; use MailPoet\API\REST\API;
use MailPoet\Automation\Engine\API\EndpointContainer; use MailPoet\API\REST\EndpointContainer;
use MailPoet\Automation\Engine\API\Request; use MailPoet\API\REST\Request;
use MailPoet\Automation\Engine\WordPress;
use MailPoet\REST\Automation\API\Endpoints\Endpoint; use MailPoet\REST\Automation\API\Endpoints\Endpoint;
use MailPoet\REST\Test; use MailPoet\REST\Test;
use MailPoet\WP\Functions as WPFunctions;
class EndpointTest extends Test { class EndpointTest extends Test {
/** @var string */ /** @var string */
private $prefix = '/mailpoet/v1/automation/mailpoet-api-testing-route'; private $prefix = '/mailpoet/v1/mailpoet-api-testing-route';
public function testGetParams(): void { public function testGetParams(): void {
$path = strtolower(__FUNCTION__); $path = strtolower(__FUNCTION__);
@ -127,7 +127,7 @@ class EndpointTest extends Test {
return new Endpoint($requestCallback); return new Endpoint($requestCallback);
} }
]), ]),
$this->diContainer->get(WordPress::class) $this->diContainer->get(WPFunctions::class)
); );
} }
} }