diff --git a/application/Espo/Core/Api/MiddlewareProvider.php b/application/Espo/Core/Api/MiddlewareProvider.php new file mode 100644 index 0000000000..f3da6b5e7b --- /dev/null +++ b/application/Espo/Core/Api/MiddlewareProvider.php @@ -0,0 +1,112 @@ +createFromClassNameList($this->getGlobalMiddlewareClassNameList()); + } + + /** + * @return MiddlewareInterface[] + */ + public function getRouteMiddlewareList(Route $route): array + { + $key = strtolower($route->getMethod()) . '_' . $route->getRoute(); + + /** @var class-string[] $classNameList */ + $classNameList = $this->metadata->get(['app', 'api', 'routeMiddlewareClassNameListMap', $key]) ?? []; + + return $this->createFromClassNameList($classNameList); + } + + /** + * @return MiddlewareInterface[] + */ + public function getControllerMiddlewareList(string $controller): array + { + /** @var class-string[] $classNameList */ + $classNameList = $this->metadata + ->get(['app', 'api', 'controllerMiddlewareClassNameListMap', $controller]) ?? []; + + return $this->createFromClassNameList($classNameList); + } + + /** + * @return MiddlewareInterface[] + */ + public function getControllerActionMiddlewareList(string $method, string $controller, string $action): array + { + $key = $controller . '_' . strtolower($method) . '_' . $action; + + /** @var class-string[] $classNameList */ + $classNameList = $this->metadata + ->get(['app', 'api', 'controllerActionMiddlewareClassNameListMap', $key]) ?? []; + + return $this->createFromClassNameList($classNameList); + } + + /** + * @return class-string[] + */ + private function getGlobalMiddlewareClassNameList(): array + { + return $this->metadata->get(['app', 'api', 'globalMiddlewareClassNameList']) ?? []; + } + + /** + * @param class-string[] $classNameList + * @return MiddlewareInterface[] + */ + private function createFromClassNameList(array $classNameList): array + { + $list = []; + + foreach ($classNameList as $className) { + $list[] = $this->injectableFactory->create($className); + } + + return $list; + } +} diff --git a/application/Espo/Core/Api/RequestProcessor.php b/application/Espo/Core/Api/RequestProcessor.php index a3d5832792..f6e2ad00bc 100644 --- a/application/Espo/Core/Api/RequestProcessor.php +++ b/application/Espo/Core/Api/RequestProcessor.php @@ -62,7 +62,7 @@ class RequestProcessor $this->processInternal($route, $request, $response); } catch (Exception $exception) { - $this->handleException($exception, $request, $response, $route->getRoute()); + $this->handleException($exception, $request, $response, $route->getAdjustedRoute()); } } diff --git a/application/Espo/Core/Api/Route.php b/application/Espo/Core/Api/Route.php index b1d7571271..13d26f093f 100644 --- a/application/Espo/Core/Api/Route.php +++ b/application/Espo/Core/Api/Route.php @@ -32,24 +32,18 @@ namespace Espo\Core\Api; class Route { private string $method; - private string $route; - /** @var array */ - private array $params; - private bool $noAuth; /** * @param array $params */ public function __construct( string $method, - string $route, - array $params, - bool $noAuth + private string $route, + private string $adjustedRoute, + private array $params, + private bool $noAuth ) { $this->method = strtoupper($method); - $this->route = $route; - $this->params = $params; - $this->noAuth = $noAuth; } public function getMethod(): string @@ -57,13 +51,24 @@ class Route return $this->method; } + /** + * Get a route. + */ public function getRoute(): string { return $this->route; } /** - * @return array + * Get an adjusted route for FastRoute. + */ + public function getAdjustedRoute(): string + { + return $this->adjustedRoute; + } + + /** + * @return array */ public function getParams(): array { diff --git a/application/Espo/Core/Api/RouteHandler.php b/application/Espo/Core/Api/RouteHandler.php new file mode 100644 index 0000000000..df8ff6c032 --- /dev/null +++ b/application/Espo/Core/Api/RouteHandler.php @@ -0,0 +1,58 @@ + $routeParams + */ + public function __construct( + private Route $route, + private array $routeParams, + private string $basePath, + private ResponseInterface $response, + private RequestProcessor $requestProcessor + ) {} + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $requestWrapped = new RequestWrapper($request, $this->basePath, $this->routeParams); + $responseWrapped = new ResponseWrapper($this->response); + + $this->requestProcessor->process($this->route, $requestWrapped, $responseWrapped); + + return $responseWrapped->getResponse(); + } +} diff --git a/application/Espo/Core/Api/Starter.php b/application/Espo/Core/Api/Starter.php index cda02f06fb..3e8ff8b39b 100644 --- a/application/Espo/Core/Api/Starter.php +++ b/application/Espo/Core/Api/Starter.php @@ -38,6 +38,7 @@ use Slim\Factory\AppFactory as SlimAppFactory; use Psr\Http\Message\ResponseInterface as Psr7Response; use Psr\Http\Message\ServerRequestInterface as Psr7Request; +use Slim\MiddlewareDispatcher; /** * API request processing entry point. @@ -48,6 +49,7 @@ class Starter private RequestProcessor $requestProcessor, private RouteUtil $routeUtil, private RouteParamsFetcher $routeParamsFetcher, + private MiddlewareProvider $middlewareProvider, private Log $log ) {} @@ -56,12 +58,20 @@ class Starter $slim = SlimAppFactory::create(); $slim->setBasePath(RouteUtil::detectBasePath()); + $this->addGlobalMiddlewares($slim); $slim->addRoutingMiddleware(); $this->addRoutes($slim); $slim->addErrorMiddleware(false, true, true, $this->log); $slim->run(); } + private function addGlobalMiddlewares(SlimApp $slim): void + { + foreach ($this->middlewareProvider->getGlobalMiddlewareList() as $middleware) { + $slim->add($middleware); + } + } + private function addRoutes(SlimApp $slim): void { $routeList = $this->routeUtil->getFullList(); @@ -73,20 +83,62 @@ class Starter private function addRoute(SlimApp $slim, Route $item): void { - $slim->map( + $slimRoute = $slim->map( [$item->getMethod()], - $item->getRoute(), + $item->getAdjustedRoute(), function (Psr7Request $request, Psr7Response $response, array $args) use ($slim, $item) { $routeParams = $this->routeParamsFetcher->fetch($item, $args); - $requestWrapped = new RequestWrapper($request, $slim->getBasePath(), $routeParams); - $responseWrapped = new ResponseWrapper($response); + $routeHandler = new RouteHandler( + $item, + $routeParams, + $slim->getBasePath(), + $response, + $this->requestProcessor + ); - $this->requestProcessor->process($item, $requestWrapped, $responseWrapped); + $dispatcher = new MiddlewareDispatcher($routeHandler); - return $responseWrapped->getResponse(); + $this->addControllerMiddlewares($dispatcher, $item, $routeParams); + + return $dispatcher->handle($request); } ); + + $middlewareList = $this->middlewareProvider->getRouteMiddlewareList($item); + + foreach ($middlewareList as $middleware) { + $slimRoute->addMiddleware($middleware); + } + } + + /** + * @param array $routeParams + */ + private function addControllerMiddlewares(MiddlewareDispatcher $dispatcher, Route $route, array $routeParams): void + { + $controller = $routeParams['controller'] ?? null; + $action = $routeParams['action'] ?? null; + + if (!$controller) { + return; + } + + if ($action) { + $controllerActionMiddlewareList = $this->middlewareProvider + ->getControllerActionMiddlewareList($route->getMethod(), $controller, $action); + + foreach ($controllerActionMiddlewareList as $middleware) { + $dispatcher->addMiddleware($middleware); + } + } + + $controllerMiddlewareList = $this->middlewareProvider + ->getControllerMiddlewareList($controller); + + foreach ($controllerMiddlewareList as $middleware) { + $dispatcher->addMiddleware($middleware); + } } } diff --git a/application/Espo/Core/Portal/Api/Starter.php b/application/Espo/Core/Portal/Api/Starter.php index d361b8603e..2047378b36 100644 --- a/application/Espo/Core/Portal/Api/Starter.php +++ b/application/Espo/Core/Portal/Api/Starter.php @@ -29,6 +29,7 @@ namespace Espo\Core\Portal\Api; +use Espo\Core\Api\MiddlewareProvider; use Espo\Core\Api\Starter as StarterBase; use Espo\Core\Portal\Utils\Route as RouteUtil; use Espo\Core\Api\RequestProcessor; @@ -41,12 +42,14 @@ class Starter extends StarterBase RequestProcessor $requestProcessor, RouteUtil $routeUtil, RouteParamsFetcher $routeParamsFetcher, + MiddlewareProvider $middlewareProvider, Log $log ) { parent::__construct( $requestProcessor, $routeUtil, $routeParamsFetcher, + $middlewareProvider, $log ); } diff --git a/application/Espo/Core/Portal/Utils/Route.php b/application/Espo/Core/Portal/Utils/Route.php index 3c8adadac2..56c1b787c8 100644 --- a/application/Espo/Core/Portal/Utils/Route.php +++ b/application/Espo/Core/Portal/Utils/Route.php @@ -41,7 +41,7 @@ class Route extends BaseRoute $newRouteList = []; foreach ($originalRouteList as $route) { - $path = $route->getRoute(); + $path = $route->getAdjustedRoute(); if ($path[0] !== '/') { $path = '/' . $path; @@ -51,6 +51,7 @@ class Route extends BaseRoute $newRoute = new RouteItem( $route->getMethod(), + $route->getRoute(), $path, $route->getParams(), $route->noAuth() diff --git a/application/Espo/Core/Utils/Metadata.php b/application/Espo/Core/Utils/Metadata.php index ceb4f66d43..127ed48fcb 100644 --- a/application/Espo/Core/Utils/Metadata.php +++ b/application/Espo/Core/Utils/Metadata.php @@ -69,7 +69,7 @@ class Metadata private $changedData = []; /** - * @var array + * @var array */ private $forceAppendPathList = [ ['app', 'rebuild', 'actionClassNameList'], @@ -77,6 +77,10 @@ class Metadata ['app', 'fieldProcessing', 'listLoaderClassNameList'], ['app', 'fieldProcessing', 'saverClassNameList'], ['app', 'hook', 'suppressClassNameList'], + ['app', 'api', 'globalMiddlewareClassNameList'], + ['app', 'api', 'routeMiddlewareClassNameListMap', self::ANY_KEY], + ['app', 'api', 'controllerMiddlewareClassNameListMap', self::ANY_KEY], + ['app', 'api', 'controllerActionMiddlewareClassNameListMap', self::ANY_KEY], ['recordDefs', self::ANY_KEY, 'readLoaderClassNameList'], ['recordDefs', self::ANY_KEY, 'listLoaderClassNameList'], ['recordDefs', self::ANY_KEY, 'saverClassNameList'], diff --git a/application/Espo/Core/Utils/Route.php b/application/Espo/Core/Utils/Route.php index 63991e8a0c..f096445593 100644 --- a/application/Espo/Core/Utils/Route.php +++ b/application/Espo/Core/Utils/Route.php @@ -30,42 +30,29 @@ namespace Espo\Core\Utils; use Espo\Core\Api\Route as RouteItem; +use Espo\Core\Utils\File\Manager as FileManager; +use Espo\Core\Utils\Resource\PathProvider; -use Espo\Core\{ - Utils\Config, - Utils\Metadata, - Utils\File\Manager as FileManager, - Utils\DataCache, - Utils\Resource\PathProvider, -}; - +/** + * @phpstan-type RouteArrayShape array{ + * route: string, + * adjustedRoute: string, + * method: string, + * noAuth?: bool, + * params?: array, + * } + */ class Route { - /** - * @var ?array< - * int, - * array{ - * route:string, - * method:string, - * noAuth?:bool, - * params?:array, - * } - * > - */ + /** @var ?(RouteArrayShape[]) */ private $data = null; - private string $cacheKey = 'routes'; - private string $routesFileName = 'routes.json'; private Config $config; - private Metadata $metadata; - private FileManager $fileManager; - private DataCache $dataCache; - private PathProvider $pathProvider; public function __construct( @@ -100,6 +87,7 @@ class Route return new RouteItem( $item['method'], $item['route'], + $item['adjustedRoute'], $item['params'] ?? [], $item['noAuth'] ?? false ); @@ -113,17 +101,7 @@ class Route $useCache = $this->config->get('useCache'); if ($this->dataCache->has($this->cacheKey) && $useCache) { - /** - * @var ?array< - * int, - * array{ - * route:string, - * method:string, - * noAuth?:bool, - * params?:array, - * } - * > $data - */ + /** @var ?(RouteArrayShape[]) $data */ $data = $this->dataCache->get($this->cacheKey); $this->data = $data; @@ -139,15 +117,7 @@ class Route } /** - * @return array< - * int, - * array{ - * route:string, - * method:string, - * noAuth?:bool, - * params?:array, - * } - * > + * @return RouteArrayShape[] */ private function unify(): array { @@ -174,24 +144,8 @@ class Route } /** - * @param array< - * int, - * array{ - * route:string, - * method:string, - * noAuth?:bool, - * params?:array, - * } - * > $currentData - * @return array< - * int, - * array{ - * route:string, - * method:string, - * noAuth?:bool, - * params?:array, - * } - * > + * @param RouteArrayShape[] $currentData + * @return RouteArrayShape[] */ private function addDataFromFile(array $currentData, string $routeFile): array { @@ -208,38 +162,14 @@ class Route /** * - * @param array< - * int, - * array{ - * route:string, - * method:string, - * noAuth?:bool, - * conditions?: array, - * } - * > $data - * @param array< - * int, - * array{ - * route:string, - * method:string, - * noAuth?:bool, - * conditions?: array, - * } - * > $newData - * @return array< - * int, - * array{ - * route:string, - * method:string, - * noAuth?:bool, - * conditions?: array, - * } - * > + * @param RouteArrayShape[] $data + * @param RouteArrayShape[] $newData + * @return RouteArrayShape[] */ private function appendRoutesToData(array $data, array $newData): array { foreach ($newData as $route) { - $route['route'] = $this->adjustPath($route['route']); + $route['adjustedRoute'] = $this->adjustPath($route['route']); if (isset($route['conditions'])) { $route['noAuth'] = !($route['conditions']['auth'] ?? true); @@ -266,7 +196,7 @@ class Route /** @var string $pathFormatted */ $pathFormatted = preg_replace('/\:([a-zA-Z0-9]+)/', '{${1}}', trim($path)); - if (substr($pathFormatted, 0, 1) !== '/') { + if (!str_starts_with($pathFormatted, '/')) { return '/' . $pathFormatted; } diff --git a/application/Espo/Resources/metadata/app/api.json b/application/Espo/Resources/metadata/app/api.json new file mode 100644 index 0000000000..6cf5e6d158 --- /dev/null +++ b/application/Espo/Resources/metadata/app/api.json @@ -0,0 +1,6 @@ +{ + "globalMiddlewareClassNameList": [], + "routeMiddlewareClassNameListMap": {}, + "controllerMiddlewareClassNameListMap": {}, + "controllerActionMiddlewareClassNameListMap": {} +} diff --git a/application/Espo/Resources/metadata/app/metadata.json b/application/Espo/Resources/metadata/app/metadata.json index 059348e2b2..8e47d8ad28 100644 --- a/application/Espo/Resources/metadata/app/metadata.json +++ b/application/Espo/Resources/metadata/app/metadata.json @@ -29,6 +29,7 @@ ["app", "orm"], ["app", "linkManager"], ["app", "hook"], + ["app", "api"], ["selectDefs"], ["recordDefs"], ["pdfDefs"], diff --git a/application/Espo/Resources/routes.json b/application/Espo/Resources/routes.json index c7e14aac76..f3218b989f 100644 --- a/application/Espo/Resources/routes.json +++ b/application/Espo/Resources/routes.json @@ -23,7 +23,7 @@ } }, { - "route": "I18n", + "route": "/I18n", "method": "get", "params": { "controller": "I18n" @@ -53,7 +53,7 @@ } }, { - "route": "User/passwordChangeRequest", + "route": "/User/passwordChangeRequest", "method": "post", "params": { "controller": "User", @@ -62,7 +62,7 @@ "noAuth": true }, { - "route": "User/changePasswordByRequest", + "route": "/User/changePasswordByRequest", "method": "post", "params": { "controller": "User", @@ -285,7 +285,7 @@ } }, { - "route": "Oidc/authorizationData", + "route": "/Oidc/authorizationData", "method": "get", "params": { "controller": "Oidc", @@ -294,7 +294,7 @@ "noAuth": true }, { - "route": "Oidc/backchannelLogout", + "route": "/Oidc/backchannelLogout", "method": "post", "params": { "controller": "Oidc", diff --git a/composer.json b/composer.json index 8a0db9d29f..1a073d464e 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "ext-pdo": "*", "psr/log": "1.1.*", "psr/http-message": "^1.0", - "slim/slim": "^4.7", + "slim/slim": "^4.11", "slim/psr7": "^1", "dragonmantank/cron-expression": "3.0.*", "laminas/laminas-mail": "2.20.*", diff --git a/composer.lock b/composer.lock index b7236520b6..d736c0b69a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0f62f7d5ae5e7936f467c5f00ef2954b", + "content-hash": "dc3ee098594199e87884788c3461f07b", "packages": [ { "name": "async-aws/core", @@ -4036,44 +4036,45 @@ }, { "name": "slim/slim", - "version": "4.7.1", + "version": "4.11.0", "source": { "type": "git", "url": "https://github.com/slimphp/Slim.git", - "reference": "0905e0775f8c1cfb3bbcfabeb6588dcfd8b82d3f" + "reference": "b0f4ca393ea037be9ac7292ba7d0a34d18bac0c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slimphp/Slim/zipball/0905e0775f8c1cfb3bbcfabeb6588dcfd8b82d3f", - "reference": "0905e0775f8c1cfb3bbcfabeb6588dcfd8b82d3f", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/b0f4ca393ea037be9ac7292ba7d0a34d18bac0c7", + "reference": "b0f4ca393ea037be9ac7292ba7d0a34d18bac0c7", "shasum": "" }, "require": { "ext-json": "*", "nikic/fast-route": "^1.3", - "php": "^7.2 || ^8.0", - "psr/container": "^1.0", + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.0", "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", - "psr/log": "^1.1" + "psr/log": "^1.1 || ^2.0 || ^3.0" }, "require-dev": { - "adriansuter/php-autoload-override": "^1.2", + "adriansuter/php-autoload-override": "^1.3", "ext-simplexml": "*", - "guzzlehttp/psr7": "^1.7", - "http-interop/http-factory-guzzle": "^1.0", - "laminas/laminas-diactoros": "^2.4", - "nyholm/psr7": "^1.3", - "nyholm/psr7-server": "^1.0.1", - "phpspec/prophecy": "^1.12", - "phpstan/phpstan": "^0.12.58", - "phpunit/phpunit": "^8.5.13", + "guzzlehttp/psr7": "^2.4", + "httpsoft/http-message": "^1.0", + "httpsoft/http-server-request": "^1.0", + "laminas/laminas-diactoros": "^2.17", + "nyholm/psr7": "^1.5", + "nyholm/psr7-server": "^1.0", + "phpspec/prophecy": "^1.15", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/phpstan": "^1.8", + "phpunit/phpunit": "^9.5", "slim/http": "^1.2", - "slim/psr7": "^1.3", - "squizlabs/php_codesniffer": "^3.5", - "weirdan/prophecy-shim": "^1.0 || ^2.0.2" + "slim/psr7": "^1.5", + "squizlabs/php_codesniffer": "^3.7" }, "suggest": { "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware", @@ -4146,7 +4147,7 @@ "type": "tidelift" } ], - "time": "2020-12-01T19:41:22+00:00" + "time": "2022-11-06T16:33:39+00:00" }, { "name": "spatie/async", @@ -7795,5 +7796,5 @@ "ext-pdo": "*" }, "platform-dev": [], - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.3.0" }