Files
espocrm/application/Espo/Core/Api/RouteProcessor.php
2026-01-19 17:46:06 +01:00

276 lines
8.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Api;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Authentication\AuthenticationFactory;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Log;
use Espo\Core\ApplicationUser;
use Psr\Http\Message\ResponseInterface as Psr7Response;
use Psr\Http\Message\ServerRequestInterface as Psr7Request;
use Slim\MiddlewareDispatcher;
use Throwable;
use LogicException;
use Exception;
/**
* Processes routes. Handles authentication. Obtains a controller name, action, body from a request.
* Then processes a controller action or an action.
*
* @internal
*/
class RouteProcessor
{
public function __construct(
private AuthenticationFactory $authenticationFactory,
private AuthBuilderFactory $authBuilderFactory,
private ErrorOutput $errorOutput,
private Config $config,
private Log $log,
private ApplicationUser $applicationUser,
private ControllerActionProcessor $actionProcessor,
private MiddlewareProvider $middlewareProvider,
private InjectableFactory $injectableFactory
) {}
public function process(
ProcessData $processData,
Psr7Request $request,
Psr7Response $response
): Psr7Response {
$requestWrapped = new RequestWrapper($request, $processData->getBasePath(), $processData->getRouteParams());
$responseWrapped = new ResponseWrapper($response);
try {
return $this->processInternal(
$processData,
$request,
$requestWrapped,
$responseWrapped
);
} catch (Exception $exception) {
$this->handleException(
$exception,
$requestWrapped,
$responseWrapped,
$processData->getRoute()->getAdjustedRoute()
);
return $responseWrapped->toPsr7();
}
}
/**
* @throws BadRequest
*/
private function processInternal(
ProcessData $processData,
Psr7Request $psrRequest,
RequestWrapper $request,
ResponseWrapper $response
): Psr7Response {
$authRequired = !$processData->getRoute()->noAuth();
$apiAuth = $this->authBuilderFactory
->create()
->setAuthentication($this->authenticationFactory->create())
->setAuthRequired($authRequired)
->build();
$authResult = $apiAuth->process($request, $response);
if (!$authResult->isResolved()) {
return $response->toPsr7();
}
if ($authResult->isResolvedUseNoAuth()) {
$this->applicationUser->setupSystemUser();
}
ob_start();
$response = $this->processAfterAuth($processData, $psrRequest, $response);
ob_clean();
return $response;
}
/**
* @throws BadRequest
*/
private function processAfterAuth(
ProcessData $processData,
Psr7Request $request,
ResponseWrapper $responseWrapped
): Psr7Response {
$actionClassName = $processData->getRoute()->getActionClassName();
if ($actionClassName) {
return $this->processAction($actionClassName, $processData, $request, $responseWrapped);
}
return $this->processControllerAction($processData, $request, $responseWrapped);
}
/**
* @param class-string<Action> $actionClassName
*/
private function processAction(
string $actionClassName,
ProcessData $processData,
Psr7Request $request,
ResponseWrapper $responseWrapped
): Psr7Response {
/** @var Action $action */
$action = $this->injectableFactory->create($actionClassName);
$handler = new ActionHandler(
action: $action,
processData: $processData,
config: $this->config,
);
$dispatcher = new MiddlewareDispatcher($handler);
foreach ($this->middlewareProvider->getActionMiddlewareList($processData->getRoute()) as $middleware) {
$dispatcher->addMiddleware($middleware);
}
$response = $dispatcher->handle($request);
// Apply headers added by the authentication.
foreach ($responseWrapped->getHeaderNames() as $name) {
$response = $response->withHeader($name, $responseWrapped->getHeaderAsArray($name));
}
return $response;
}
/**
* @throws BadRequest
*/
private function processControllerAction(
ProcessData $processData,
Psr7Request $request,
ResponseWrapper $responseWrapped
): Psr7Response {
$controller = $this->getControllerName($processData);
$action = $processData->getRouteParams()['action'] ?? null;
$method = $request->getMethod();
if (!$action) {
$crudMethodActionMap = $this->config->get('crud') ?? [];
$action = $crudMethodActionMap[strtolower($method)] ?? null;
if (!$action) {
throw new BadRequest("No action for method `$method`.");
}
}
$handler = new ControllerActionHandler(
controllerName: $controller,
actionName: $action,
processData: $processData,
responseWrapped: $responseWrapped,
controllerActionProcessor: $this->actionProcessor,
config: $this->config,
);
$dispatcher = new MiddlewareDispatcher($handler);
$this->addControllerMiddlewares($dispatcher, $method, $controller, $action);
return $dispatcher->handle($request);
}
private function getControllerName(ProcessData $processData): string
{
$controllerName = $processData->getRouteParams()['controller'] ?? null;
if (!$controllerName) {
throw new LogicException("Route doesn't have specified controller.");
}
return ucfirst($controllerName);
}
private function handleException(
Exception $exception,
Request $request,
Response $response,
string $route
): void {
try {
$this->errorOutput->process($request, $response, $exception, $route);
} catch (Throwable $exceptionAnother) {
$this->log->error($exceptionAnother->getMessage());
$response->setStatus(500);
}
}
/**
* @param MiddlewareDispatcher<null> $dispatcher
*/
private function addControllerMiddlewares(
MiddlewareDispatcher $dispatcher,
string $method,
string $controller,
string $action
): void {
$controllerActionMiddlewareList = $this->middlewareProvider
->getControllerActionMiddlewareList($method, $controller, $action);
foreach ($controllerActionMiddlewareList as $middleware) {
$dispatcher->addMiddleware($middleware);
}
$controllerMiddlewareList = $this->middlewareProvider
->getControllerMiddlewareList($controller);
foreach ($controllerMiddlewareList as $middleware) {
$dispatcher->addMiddleware($middleware);
}
}
}