middleware api

This commit is contained in:
Yuri Kuznetsov
2022-12-25 10:28:51 +02:00
parent 008935f054
commit 28d8fcd31e
14 changed files with 313 additions and 140 deletions
@@ -0,0 +1,112 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Api;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use Psr\Http\Server\MiddlewareInterface;
class MiddlewareProvider
{
public function __construct(
private Metadata $metadata,
private InjectableFactory $injectableFactory
) {}
/**
* @return MiddlewareInterface[]
*/
public function getGlobalMiddlewareList(): array
{
return $this->createFromClassNameList($this->getGlobalMiddlewareClassNameList());
}
/**
* @return MiddlewareInterface[]
*/
public function getRouteMiddlewareList(Route $route): array
{
$key = strtolower($route->getMethod()) . '_' . $route->getRoute();
/** @var class-string<MiddlewareInterface>[] $classNameList */
$classNameList = $this->metadata->get(['app', 'api', 'routeMiddlewareClassNameListMap', $key]) ?? [];
return $this->createFromClassNameList($classNameList);
}
/**
* @return MiddlewareInterface[]
*/
public function getControllerMiddlewareList(string $controller): array
{
/** @var class-string<MiddlewareInterface>[] $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<MiddlewareInterface>[] $classNameList */
$classNameList = $this->metadata
->get(['app', 'api', 'controllerActionMiddlewareClassNameListMap', $key]) ?? [];
return $this->createFromClassNameList($classNameList);
}
/**
* @return class-string<MiddlewareInterface>[]
*/
private function getGlobalMiddlewareClassNameList(): array
{
return $this->metadata->get(['app', 'api', 'globalMiddlewareClassNameList']) ?? [];
}
/**
* @param class-string<MiddlewareInterface>[] $classNameList
* @return MiddlewareInterface[]
*/
private function createFromClassNameList(array $classNameList): array
{
$list = [];
foreach ($classNameList as $className) {
$list[] = $this->injectableFactory->create($className);
}
return $list;
}
}
@@ -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());
}
}
+16 -11
View File
@@ -32,24 +32,18 @@ namespace Espo\Core\Api;
class Route
{
private string $method;
private string $route;
/** @var array<string, string> */
private array $params;
private bool $noAuth;
/**
* @param array<string, string> $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<string,string>
* Get an adjusted route for FastRoute.
*/
public function getAdjustedRoute(): string
{
return $this->adjustedRoute;
}
/**
* @return array<string, string>
*/
public function getParams(): array
{
@@ -0,0 +1,58 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Api;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class RouteHandler implements RequestHandlerInterface
{
/**
* @param array<string, mixed> $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();
}
}
+58 -6
View File
@@ -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<string, mixed> $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);
}
}
}
@@ -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
);
}
+2 -1
View File
@@ -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()
+5 -1
View File
@@ -69,7 +69,7 @@ class Metadata
private $changedData = [];
/**
* @var array<int,string[]>
* @var array<int, string[]>
*/
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'],
+22 -92
View File
@@ -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<string, mixed>,
* }
*/
class Route
{
/**
* @var ?array<
* int,
* array{
* route:string,
* method:string,
* noAuth?:bool,
* params?:array<string,mixed>,
* }
* >
*/
/** @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<string,mixed>,
* }
* > $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<string,mixed>,
* }
* >
* @return RouteArrayShape[]
*/
private function unify(): array
{
@@ -174,24 +144,8 @@ class Route
}
/**
* @param array<
* int,
* array{
* route:string,
* method:string,
* noAuth?:bool,
* params?:array<string,mixed>,
* }
* > $currentData
* @return array<
* int,
* array{
* route:string,
* method:string,
* noAuth?:bool,
* params?:array<string,mixed>,
* }
* >
* @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<string,mixed>,
* }
* > $data
* @param array<
* int,
* array{
* route:string,
* method:string,
* noAuth?:bool,
* conditions?: array<string,mixed>,
* }
* > $newData
* @return array<
* int,
* array{
* route:string,
* method:string,
* noAuth?:bool,
* conditions?: array<string,mixed>,
* }
* >
* @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;
}
@@ -0,0 +1,6 @@
{
"globalMiddlewareClassNameList": [],
"routeMiddlewareClassNameListMap": {},
"controllerMiddlewareClassNameListMap": {},
"controllerActionMiddlewareClassNameListMap": {}
}
@@ -29,6 +29,7 @@
["app", "orm"],
["app", "linkManager"],
["app", "hook"],
["app", "api"],
["selectDefs"],
["recordDefs"],
["pdfDefs"],
+5 -5
View File
@@ -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",
+1 -1
View File
@@ -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.*",
Generated
+23 -22
View File
@@ -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"
}