Initial commit
This commit is contained in:
55
application/Espo/Core/Api/Action.php
Normal file
55
application/Espo/Core/Api/Action.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?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\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
/**
|
||||
* A route action.
|
||||
*/
|
||||
interface Action
|
||||
{
|
||||
/**
|
||||
* Process.
|
||||
*
|
||||
* @param Request $request A request.
|
||||
* @return Response A response. Use ResponseComposer for building.
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Conflict
|
||||
* @throws Error
|
||||
*/
|
||||
public function process(Request $request): Response;
|
||||
}
|
||||
122
application/Espo/Core/Api/ActionHandler.php
Normal file
122
application/Espo/Core/Api/ActionHandler.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?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\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
use Espo\Core\Utils\Config;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Psr7Response;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Slim\Psr7\Factory\ResponseFactory;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ActionHandler implements RequestHandlerInterface
|
||||
{
|
||||
private const DEFAULT_CONTENT_TYPE = 'application/json';
|
||||
|
||||
public function __construct(
|
||||
private Action $action,
|
||||
private ProcessData $processData,
|
||||
private Config $config
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$requestWrapped = new RequestWrapper(
|
||||
$request,
|
||||
$this->processData->getBasePath(),
|
||||
$this->processData->getRouteParams()
|
||||
);
|
||||
|
||||
$response = $this->action->process($requestWrapped);
|
||||
|
||||
return $this->prepareResponse($response);
|
||||
}
|
||||
|
||||
private function prepareResponse(Response $response): Psr7Response
|
||||
{
|
||||
if (!$response->hasHeader('Content-Type')) {
|
||||
$response->setHeader('Content-Type', self::DEFAULT_CONTENT_TYPE);
|
||||
}
|
||||
|
||||
if (!$response->hasHeader('Cache-Control')) {
|
||||
$response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
|
||||
}
|
||||
|
||||
if (!$response->hasHeader('Expires')) {
|
||||
$response->setHeader('Expires', '0');
|
||||
}
|
||||
|
||||
if (!$response->hasHeader('Last-Modified')) {
|
||||
$response->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
|
||||
}
|
||||
|
||||
$response->setHeader('X-App-Timestamp', (string) ($this->config->get('appTimestamp') ?? '0'));
|
||||
|
||||
/** @noinspection PhpConditionAlreadyCheckedInspection */
|
||||
return $response instanceof ResponseWrapper ?
|
||||
$response->toPsr7() :
|
||||
self::responseToPsr7($response);
|
||||
}
|
||||
|
||||
private static function responseToPsr7(Response $response): Psr7Response
|
||||
{
|
||||
$psr7Response = (new ResponseFactory())->createResponse();
|
||||
|
||||
$statusCode = $response->getStatusCode();
|
||||
$reason = $response->getReasonPhrase();
|
||||
$body = $response->getBody();
|
||||
|
||||
$psr7Response = $psr7Response
|
||||
->withStatus($statusCode, $reason)
|
||||
->withBody($body);
|
||||
|
||||
foreach ($response->getHeaderNames() as $name) {
|
||||
$psr7Response = $psr7Response->withHeader($name, $response->getHeaderAsArray($name));
|
||||
}
|
||||
|
||||
return $psr7Response;
|
||||
}
|
||||
}
|
||||
347
application/Espo/Core/Api/Auth.php
Normal file
347
application/Espo/Core/Api/Auth.php
Normal file
@@ -0,0 +1,347 @@
|
||||
<?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\Authentication\HeaderKey;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\ServiceUnavailable;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Authentication\ConfigDataProvider;
|
||||
use Espo\Core\Authentication\Authentication;
|
||||
use Espo\Core\Authentication\AuthenticationData;
|
||||
use Espo\Core\Authentication\Result;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Utils\Json;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Determines which auth method to use. Fetches a username and password from headers and server parameters.
|
||||
* Then tries to log in.
|
||||
*/
|
||||
class Auth
|
||||
{
|
||||
public function __construct(
|
||||
private Log $log,
|
||||
private Authentication $authentication,
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
private bool $authRequired = true,
|
||||
private bool $isEntryPoint = false
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Exception
|
||||
*/
|
||||
public function process(Request $request, Response $response): AuthResult
|
||||
{
|
||||
$username = null;
|
||||
$password = null;
|
||||
|
||||
$authenticationMethod = $this->obtainAuthenticationMethodFromRequest($request);
|
||||
|
||||
if (!$authenticationMethod) {
|
||||
[$username, $password] = $this->obtainUsernamePasswordFromRequest($request);
|
||||
}
|
||||
|
||||
$authenticationData = AuthenticationData::create()
|
||||
->withUsername($username)
|
||||
->withPassword($password)
|
||||
->withMethod($authenticationMethod);
|
||||
|
||||
$hasAuthData = $username || $authenticationMethod;
|
||||
|
||||
if (!$hasAuthData) {
|
||||
$password = $this->obtainTokenFromCookies($request);
|
||||
|
||||
if ($password) {
|
||||
$authenticationData = AuthenticationData::create()
|
||||
->withPassword($password)
|
||||
->withByTokenOnly(true);
|
||||
|
||||
$hasAuthData = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->authRequired && !$this->isEntryPoint && $hasAuthData) {
|
||||
$authResult = $this->processAuthNotRequired(
|
||||
$authenticationData,
|
||||
$request,
|
||||
$response
|
||||
);
|
||||
|
||||
if ($authResult) {
|
||||
return $authResult;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->authRequired) {
|
||||
return AuthResult::createResolvedUseNoAuth();
|
||||
}
|
||||
|
||||
if ($hasAuthData) {
|
||||
return $this->processWithAuthData($authenticationData, $request, $response);
|
||||
}
|
||||
|
||||
$showDialog =
|
||||
($this->isEntryPoint || !$this->isXMLHttpRequest($request)) &&
|
||||
!$request->getHeader('Referer') &&
|
||||
$request->getMethod() !== Method::POST;
|
||||
|
||||
$this->handleUnauthorized($response, null, $showDialog);
|
||||
|
||||
return AuthResult::createNotResolved();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function processAuthNotRequired(
|
||||
AuthenticationData $data,
|
||||
Request $request,
|
||||
Response $response
|
||||
): ?AuthResult {
|
||||
|
||||
try {
|
||||
$result = $this->authentication->login($data, $request, $response);
|
||||
} catch (Exception $e) {
|
||||
$this->handleException($response, $e);
|
||||
|
||||
return AuthResult::createNotResolved();
|
||||
}
|
||||
|
||||
if (!$result->isFail()) {
|
||||
return AuthResult::createResolved();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function processWithAuthData(
|
||||
AuthenticationData $data,
|
||||
Request $request,
|
||||
Response $response
|
||||
): AuthResult {
|
||||
|
||||
try {
|
||||
$result = $this->authentication->login($data, $request, $response);
|
||||
} catch (Exception $e) {
|
||||
$this->handleException($response, $e);
|
||||
|
||||
return AuthResult::createNotResolved();
|
||||
}
|
||||
|
||||
if ($result->isSuccess()) {
|
||||
return AuthResult::createResolved();
|
||||
}
|
||||
|
||||
if ($result->isFail()) {
|
||||
$showDialog =
|
||||
$this->isEntryPoint &&
|
||||
!$request->getHeader('Referer') &&
|
||||
$request->getMethod() !== Method::POST;
|
||||
|
||||
$this->handleUnauthorized($response, $result, $showDialog);
|
||||
}
|
||||
|
||||
if ($result->isSecondStepRequired()) {
|
||||
$this->handleSecondStepRequired($response, $result);
|
||||
}
|
||||
|
||||
return AuthResult::createNotResolved();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{string, string}
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function decodeAuthorizationString(string $string): array
|
||||
{
|
||||
/** @var string $stringDecoded */
|
||||
$stringDecoded = base64_decode($string);
|
||||
|
||||
if (!str_contains($stringDecoded, ':')) {
|
||||
throw new BadRequest("Auth: Bad authorization string provided.");
|
||||
}
|
||||
|
||||
[$username, $password] = explode(':', $stringDecoded, 2);
|
||||
|
||||
$username = trim($username);
|
||||
$password = trim($password);
|
||||
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
private function handleSecondStepRequired(Response $response, Result $result): void
|
||||
{
|
||||
$response->setStatus(401);
|
||||
$response->setHeader('X-Status-Reason', 'second-step-required');
|
||||
|
||||
$bodyData = [
|
||||
'status' => $result->getStatus(),
|
||||
'message' => $result->getMessage(),
|
||||
'view' => $result->getView(),
|
||||
'token' => $result->getToken(),
|
||||
'data' => $result->getData(),
|
||||
];
|
||||
|
||||
$response->writeBody(Json::encode($bodyData));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function handleException(Response $response, Exception $e): void
|
||||
{
|
||||
if (
|
||||
$e instanceof BadRequest ||
|
||||
$e instanceof ServiceUnavailable ||
|
||||
$e instanceof Forbidden
|
||||
) {
|
||||
$reason = $e->getMessage();
|
||||
|
||||
if ($reason) {
|
||||
$response->setHeader('X-Status-Reason', $e->getMessage());
|
||||
}
|
||||
|
||||
$response->setStatus($e->getCode());
|
||||
|
||||
if ($e->getBody()) {
|
||||
$response->writeBody($e->getBody());
|
||||
}
|
||||
|
||||
if ($e->getMessage()) {
|
||||
$this->log->notice("Auth exception: {message}", ['message' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
private function handleUnauthorized(Response $response, ?Result $result, bool $showDialog): void
|
||||
{
|
||||
if ($showDialog) {
|
||||
$response->setHeader('WWW-Authenticate', 'Basic realm=""');
|
||||
}
|
||||
|
||||
if ($result && $result->getFailReason() === Result\FailReason::ERROR) {
|
||||
$response = $response->setHeader('X-Status-Reason', 'error');
|
||||
}
|
||||
|
||||
$response->setStatus(401);
|
||||
}
|
||||
|
||||
private function isXMLHttpRequest(Request $request): bool
|
||||
{
|
||||
if (strtolower($request->getHeader('X-Requested-With') ?? '') == 'xmlhttprequest') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function obtainAuthenticationMethodFromRequest(Request $request): ?string
|
||||
{
|
||||
if ($request->hasHeader(HeaderKey::AUTHORIZATION)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$paramsList = array_values(array_filter(
|
||||
$this->configDataProvider->getLoginMetadataParamsList(),
|
||||
function ($params) use ($request): bool {
|
||||
$header = $params->getCredentialsHeader();
|
||||
|
||||
if (!$header || !$params->isApi()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $request->hasHeader($header);
|
||||
}
|
||||
));
|
||||
|
||||
if (count($paramsList)) {
|
||||
return $paramsList[0]->getMethod();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{?string, ?string}
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function obtainUsernamePasswordFromRequest(Request $request): array
|
||||
{
|
||||
if ($request->hasHeader(HeaderKey::AUTHORIZATION)) {
|
||||
$headerValue = $request->getHeader(HeaderKey::AUTHORIZATION) ?? '';
|
||||
|
||||
return $this->decodeAuthorizationString($headerValue);
|
||||
}
|
||||
|
||||
if (
|
||||
$request->getServerParam('PHP_AUTH_USER') &&
|
||||
$request->getServerParam('PHP_AUTH_PW')
|
||||
) {
|
||||
$username = $request->getServerParam('PHP_AUTH_USER');
|
||||
$password = $request->getServerParam('PHP_AUTH_PW');
|
||||
|
||||
if (is_string($username)) {
|
||||
$username = trim($username);
|
||||
}
|
||||
|
||||
if (is_string($password)) {
|
||||
$password = trim($password);
|
||||
}
|
||||
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
$cgiAuthString = $request->getHeader('Http-Espo-Cgi-Auth') ??
|
||||
$request->getHeader('Redirect-Http-Espo-Cgi-Auth');
|
||||
|
||||
if ($cgiAuthString) {
|
||||
[$username, $password] = $this->decodeAuthorizationString(substr($cgiAuthString, 6));
|
||||
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
private function obtainTokenFromCookies(Request $request): ?string
|
||||
{
|
||||
return $request->getCookieParam('auth-token');
|
||||
}
|
||||
}
|
||||
91
application/Espo/Core/Api/AuthBuilder.php
Normal file
91
application/Espo/Core/Api/AuthBuilder.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?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\Authentication\Authentication;
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\Binding\ContextualBinder;
|
||||
use Espo\Core\InjectableFactory;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Builds Auth instance.
|
||||
*/
|
||||
class AuthBuilder
|
||||
{
|
||||
private bool $authRequired = false;
|
||||
private bool $isEntryPoint = false;
|
||||
private ?Authentication $authentication = null;
|
||||
|
||||
public function __construct(private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
public function setAuthentication(Authentication $authentication): self
|
||||
{
|
||||
$this->authentication = $authentication;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAuthRequired(bool $authRequired): self
|
||||
{
|
||||
$this->authRequired = $authRequired;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function forEntryPoint(): self
|
||||
{
|
||||
$this->isEntryPoint = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function build(): Auth
|
||||
{
|
||||
if (!$this->authentication) {
|
||||
throw new RuntimeException("Authentication is not set.");
|
||||
}
|
||||
|
||||
return $this->injectableFactory->createWithBinding(
|
||||
Auth::class,
|
||||
BindingContainerBuilder
|
||||
::create()
|
||||
->bindInstance(Authentication::class, $this->authentication)
|
||||
->inContext(Auth::class, function (ContextualBinder $binder) {
|
||||
$binder
|
||||
->bindValue('$authRequired', $this->authRequired)
|
||||
->bindValue('$isEntryPoint', $this->isEntryPoint);
|
||||
})
|
||||
->build()
|
||||
);
|
||||
}
|
||||
}
|
||||
43
application/Espo/Core/Api/AuthBuilderFactory.php
Normal file
43
application/Espo/Core/Api/AuthBuilderFactory.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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\InjectableFactory;
|
||||
|
||||
class AuthBuilderFactory
|
||||
{
|
||||
public function __construct(private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
public function create(): AuthBuilder
|
||||
{
|
||||
return $this->injectableFactory->create(AuthBuilder::class);
|
||||
}
|
||||
}
|
||||
79
application/Espo/Core/Api/AuthResult.php
Normal file
79
application/Espo/Core/Api/AuthResult.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* An authentication result.
|
||||
*/
|
||||
class AuthResult
|
||||
{
|
||||
private bool $isResolved = false;
|
||||
private bool $isResolvedUseNoAuth = false;
|
||||
|
||||
public static function createResolved(): self
|
||||
{
|
||||
$obj = new self();
|
||||
|
||||
$obj->isResolved = true;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public static function createResolvedUseNoAuth(): self
|
||||
{
|
||||
$obj = new self();
|
||||
|
||||
$obj->isResolved = true;
|
||||
$obj->isResolvedUseNoAuth = true;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public static function createNotResolved(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logged in successfully.
|
||||
*/
|
||||
public function isResolved(): bool
|
||||
{
|
||||
return $this->isResolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* No need to log in.
|
||||
*/
|
||||
public function isResolvedUseNoAuth(): bool
|
||||
{
|
||||
return $this->isResolvedUseNoAuth;
|
||||
}
|
||||
}
|
||||
90
application/Espo/Core/Api/ControllerActionHandler.php
Normal file
90
application/Espo/Core/Api/ControllerActionHandler.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?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\NotFound;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ControllerActionHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private string $controllerName,
|
||||
private string $actionName,
|
||||
private ProcessData $processData,
|
||||
private ResponseWrapper $responseWrapped,
|
||||
private ControllerActionProcessor $controllerActionProcessor,
|
||||
private Config $config
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$requestWrapped = new RequestWrapper(
|
||||
$request,
|
||||
$this->processData->getBasePath(),
|
||||
$this->processData->getRouteParams()
|
||||
);
|
||||
|
||||
$this->beforeProceed();
|
||||
|
||||
$responseWrapped = $this->controllerActionProcessor->process(
|
||||
$this->controllerName,
|
||||
$this->actionName,
|
||||
$requestWrapped,
|
||||
$this->responseWrapped
|
||||
);
|
||||
|
||||
$this->afterProceed($responseWrapped);
|
||||
|
||||
return $responseWrapped->toPsr7();
|
||||
}
|
||||
|
||||
private function beforeProceed(): void
|
||||
{
|
||||
$this->responseWrapped->setHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
private function afterProceed(Response $responseWrapped): void
|
||||
{
|
||||
$responseWrapped
|
||||
->setHeader('X-App-Timestamp', (string) ($this->config->get('appTimestamp') ?? '0'))
|
||||
->setHeader('Expires', '0')
|
||||
->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT')
|
||||
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
|
||||
}
|
||||
}
|
||||
217
application/Espo/Core/Api/ControllerActionProcessor.php
Normal file
217
application/Espo/Core/Api/ControllerActionProcessor.php
Normal file
@@ -0,0 +1,217 @@
|
||||
<?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\NotFound;
|
||||
use Espo\Core\Exceptions\NotFoundSilent;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\ClassFinder;
|
||||
use Espo\Core\Utils\Json;
|
||||
|
||||
use ReflectionClass;
|
||||
use ReflectionNamedType;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Creates controller instances and processes actions.
|
||||
*/
|
||||
class ControllerActionProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private ClassFinder $classFinder
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function process(
|
||||
string $controllerName,
|
||||
string $actionName,
|
||||
Request $request,
|
||||
ResponseWrapper $response
|
||||
): ResponseWrapper {
|
||||
|
||||
$controller = $this->createController($controllerName);
|
||||
|
||||
$requestMethod = $request->getMethod();
|
||||
|
||||
if (
|
||||
$actionName == 'index' &&
|
||||
property_exists($controller, 'defaultAction')
|
||||
) {
|
||||
$actionName = $controller::$defaultAction ?? 'index';
|
||||
}
|
||||
|
||||
$actionMethodName = 'action' . ucfirst($actionName);
|
||||
|
||||
$fullActionMethodName = strtolower($requestMethod) . ucfirst($actionMethodName);
|
||||
|
||||
$primaryActionMethodName = method_exists($controller, $fullActionMethodName) ?
|
||||
$fullActionMethodName :
|
||||
$actionMethodName;
|
||||
|
||||
if (!method_exists($controller, $primaryActionMethodName)) {
|
||||
throw new NotFoundSilent(
|
||||
"Action $requestMethod '$actionName' does not exist in controller '$controllerName'.");
|
||||
}
|
||||
|
||||
if ($this->useShortParamList($controller, $primaryActionMethodName)) {
|
||||
$result = $controller->$primaryActionMethodName($request, $response) ?? null;
|
||||
|
||||
$this->handleResult($response, $result);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Below is a legacy way.
|
||||
|
||||
$data = $request->getBodyContents();
|
||||
|
||||
if ($data && $this->getRequestContentType($request) === 'application/json') {
|
||||
$data = json_decode($data);
|
||||
}
|
||||
|
||||
$params = $request->getRouteParams();
|
||||
|
||||
$beforeMethodName = 'before' . ucfirst($actionName);
|
||||
|
||||
if (method_exists($controller, $beforeMethodName)) {
|
||||
$controller->$beforeMethodName($params, $data, $request, $response);
|
||||
}
|
||||
|
||||
$result = $controller->$primaryActionMethodName($params, $data, $request, $response) ?? null;
|
||||
|
||||
$afterMethodName = 'after' . ucfirst($actionName);
|
||||
|
||||
if (method_exists($controller, $afterMethodName)) {
|
||||
$controller->$afterMethodName($params, $data, $request, $response);
|
||||
}
|
||||
|
||||
$this->handleResult($response, $result);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $result
|
||||
*/
|
||||
private function handleResult(Response $response, $result): void
|
||||
{
|
||||
$responseContents = $result;
|
||||
|
||||
if (
|
||||
is_int($result) ||
|
||||
is_float($result) ||
|
||||
is_array($result) ||
|
||||
is_bool($result) ||
|
||||
$result instanceof stdClass
|
||||
) {
|
||||
$responseContents = Json::encode($result);
|
||||
}
|
||||
|
||||
if (is_string($responseContents)) {
|
||||
$response->writeBody($responseContents);
|
||||
}
|
||||
}
|
||||
|
||||
private function useShortParamList(object $controller, string $methodName): bool
|
||||
{
|
||||
$class = new ReflectionClass($controller);
|
||||
|
||||
$method = $class->getMethod($methodName);
|
||||
$params = $method->getParameters();
|
||||
|
||||
if (count($params) === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = $params[0]->getType();
|
||||
|
||||
if (
|
||||
!$type instanceof ReflectionNamedType ||
|
||||
$type->isBuiltin()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var class-string $className */
|
||||
$className = $type->getName();
|
||||
|
||||
$firstParamClass = new ReflectionClass($className);
|
||||
|
||||
if (
|
||||
$firstParamClass->getName() === Request::class ||
|
||||
$firstParamClass->isSubclassOf(Request::class)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function getControllerClassName(string $name): string
|
||||
{
|
||||
$className = $this->classFinder->find('Controllers', $name);
|
||||
|
||||
if (!$className) {
|
||||
throw new NotFound("Controller '$name' does not exist.");
|
||||
}
|
||||
|
||||
if (!class_exists($className)) {
|
||||
throw new NotFound("Class not found for controller '$name'.");
|
||||
}
|
||||
|
||||
return $className;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function createController(string $name): object
|
||||
{
|
||||
return $this->injectableFactory->createWith($this->getControllerClassName($name), [
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getRequestContentType(Request $request): ?string
|
||||
{
|
||||
if ($request instanceof RequestWrapper) {
|
||||
return $request->getContentType();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
278
application/Espo/Core/Api/ErrorOutput.php
Normal file
278
application/Espo/Core/Api/ErrorOutput.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?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\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\HasBody;
|
||||
use Espo\Core\Exceptions\HasLogLevel;
|
||||
use Espo\Core\Exceptions\HasLogMessage;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Utils\Log;
|
||||
|
||||
use LogicException;
|
||||
use Psr\Log\LogLevel;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Processes an error output. If an exception occurred, it will be passed to here.
|
||||
*/
|
||||
class ErrorOutput
|
||||
{
|
||||
/** @var array<int, string> */
|
||||
private $errorDescriptions = [
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Page Not Found',
|
||||
409 => 'Conflict',
|
||||
500 => 'Internal Server Error',
|
||||
503 => 'Service Unavailable',
|
||||
];
|
||||
|
||||
/** @var int[] */
|
||||
private $allowedStatusCodeList = [
|
||||
200,
|
||||
201,
|
||||
400,
|
||||
401,
|
||||
403,
|
||||
404,
|
||||
409,
|
||||
500,
|
||||
503,
|
||||
];
|
||||
|
||||
/** @var class-string<Throwable>[] */
|
||||
private array $printStatusReasonExceptionClassNameList = [
|
||||
Error::class,
|
||||
Forbidden::class,
|
||||
Conflict::class,
|
||||
BadRequest::class,
|
||||
NotFound::class,
|
||||
];
|
||||
|
||||
public function __construct(private Log $log)
|
||||
{}
|
||||
|
||||
public function process(
|
||||
Request $request,
|
||||
Response $response,
|
||||
Throwable $exception,
|
||||
?string $route = null
|
||||
): void {
|
||||
|
||||
$this->processInternal($request, $response, $exception, $route);
|
||||
}
|
||||
|
||||
public function processWithBodyPrinting(
|
||||
Request $request,
|
||||
Response $response,
|
||||
Throwable $exception,
|
||||
?string $route = null
|
||||
): void {
|
||||
|
||||
$this->processInternal($request, $response, $exception, $route, true);
|
||||
}
|
||||
|
||||
private function processInternal(
|
||||
Request $request,
|
||||
Response $response,
|
||||
Throwable $exception,
|
||||
?string $route = null,
|
||||
bool $toPrintBody = false
|
||||
): void {
|
||||
|
||||
$message = $exception->getMessage();
|
||||
|
||||
if ($exception->getPrevious() && $exception->getPrevious()->getMessage()) {
|
||||
$message .= " " . $exception->getPrevious()->getMessage();
|
||||
}
|
||||
|
||||
$statusCode = $exception->getCode();
|
||||
|
||||
if ($exception instanceof HasLogMessage) {
|
||||
$message = $exception->getLogMessage();
|
||||
}
|
||||
|
||||
if ($route) {
|
||||
$this->processRoute($route, $request, $exception);
|
||||
}
|
||||
|
||||
$level = $this->getLevel($exception);
|
||||
|
||||
$this->log->log($level, $message, [
|
||||
'exception' => $exception,
|
||||
'request' => $request,
|
||||
]);
|
||||
|
||||
if (!in_array($statusCode, $this->allowedStatusCodeList)) {
|
||||
$statusCode = 500;
|
||||
}
|
||||
|
||||
$response->setStatus($statusCode);
|
||||
|
||||
if ($this->toPrintExceptionStatusReason($exception)) {
|
||||
$response->setHeader('X-Status-Reason', $this->stripInvalidCharactersFromHeaderValue($message));
|
||||
}
|
||||
|
||||
if ($exception instanceof HasBody && $this->exceptionHasBody($exception)) {
|
||||
$response->writeBody($exception->getBody() ?? '');
|
||||
|
||||
$toPrintBody = false;
|
||||
}
|
||||
|
||||
if ($toPrintBody) {
|
||||
$codeDescription = $this->getCodeDescription($statusCode);
|
||||
|
||||
$statusText = isset($codeDescription) ?
|
||||
$statusCode . ' '. $codeDescription :
|
||||
'HTTP ' . $statusCode;
|
||||
|
||||
if ($message) {
|
||||
$message = htmlspecialchars($message);
|
||||
}
|
||||
|
||||
$response->writeBody(self::generateErrorBody($statusText, $message));
|
||||
}
|
||||
}
|
||||
|
||||
private function exceptionHasBody(Throwable $exception): bool
|
||||
{
|
||||
if (!$exception instanceof HasBody) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$exceptionBody = $exception->getBody();
|
||||
|
||||
return $exceptionBody !== null;
|
||||
}
|
||||
|
||||
private function getCodeDescription(int $statusCode): ?string
|
||||
{
|
||||
if (isset($this->errorDescriptions[$statusCode])) {
|
||||
return $this->errorDescriptions[$statusCode];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function clearPasswords(string $string): string
|
||||
{
|
||||
return preg_replace('/"(.*password.*)":".*"/i', '"$1":"*****"', $string) ?? $string;
|
||||
}
|
||||
|
||||
private static function generateErrorBody(string $header, string $text): string
|
||||
{
|
||||
$body = "<h1>" . $header . "</h1>";
|
||||
$body .= $text;
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
private function stripInvalidCharactersFromHeaderValue(string $value): string
|
||||
{
|
||||
$pattern = "/[^ \t\x21-\x7E\x80-\xFF]/";
|
||||
|
||||
/** @var string */
|
||||
return preg_replace($pattern, ' ', $value);
|
||||
}
|
||||
|
||||
private function processRoute(string $route, Request $request, Throwable $exception): void
|
||||
{
|
||||
$requestBodyString = $this->clearPasswords($request->getBodyContents() ?? '');
|
||||
|
||||
$message = $exception->getMessage();
|
||||
|
||||
if ($exception->getPrevious() && $exception->getPrevious()->getMessage()) {
|
||||
$message .= " " . $exception->getPrevious()->getMessage();
|
||||
}
|
||||
|
||||
$statusCode = $exception->getCode();
|
||||
|
||||
$routeParams = $request->getRouteParams();
|
||||
|
||||
$logMessage = "API ($statusCode) ";
|
||||
|
||||
$logMessageItemList = [];
|
||||
|
||||
if ($message) {
|
||||
$logMessageItemList[] = $message;
|
||||
}
|
||||
|
||||
$logMessageItemList[] = $request->getMethod() . ' ' . $request->getResourcePath();
|
||||
|
||||
if ($requestBodyString) {
|
||||
$logMessageItemList[] = "Input data: " . $requestBodyString;
|
||||
}
|
||||
|
||||
$logMessageItemList[] = "Route pattern: " . $route;
|
||||
|
||||
if (!empty($routeParams)) {
|
||||
$logMessageItemList[] = "Route params: " . print_r($routeParams, true);
|
||||
}
|
||||
|
||||
$logMessage .= implode("; ", $logMessageItemList);
|
||||
|
||||
$this->log->debug($logMessage);
|
||||
}
|
||||
|
||||
private function toPrintExceptionStatusReason(Throwable $exception): bool
|
||||
{
|
||||
foreach ($this->printStatusReasonExceptionClassNameList as $clasName) {
|
||||
|
||||
if ($exception instanceof ($clasName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getLevel(Throwable $exception): string
|
||||
{
|
||||
if ($exception instanceof HasLogLevel) {
|
||||
return $exception->getLogLevel();
|
||||
}
|
||||
|
||||
if ($exception instanceof LogicException) {
|
||||
return LogLevel::ALERT;
|
||||
}
|
||||
|
||||
if ($exception instanceof RuntimeException) {
|
||||
return LogLevel::CRITICAL;
|
||||
}
|
||||
|
||||
return LogLevel::ERROR;
|
||||
}
|
||||
}
|
||||
47
application/Espo/Core/Api/Method.php
Normal file
47
application/Espo/Core/Api/Method.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @since 9.2.0
|
||||
*/
|
||||
class Method
|
||||
{
|
||||
public const HEAD = 'HEAD';
|
||||
public const GET = 'GET';
|
||||
public const POST = 'POST';
|
||||
public const PUT = 'PUT';
|
||||
public const PATCH = 'PATCH';
|
||||
public const DELETE = 'DELETE';
|
||||
public const PURGE = 'PURGE';
|
||||
public const OPTIONS = 'OPTIONS';
|
||||
public const TRACE = 'TRACE';
|
||||
public const CONNECT = 'CONNECT';
|
||||
}
|
||||
128
application/Espo/Core/Api/MiddlewareProvider.php
Normal file
128
application/Espo/Core/Api/MiddlewareProvider.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?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\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
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 getActionMiddlewareList(Route $route): array
|
||||
{
|
||||
$key = strtolower($route->getMethod()) . '_' . $route->getRoute();
|
||||
|
||||
/** @var class-string<MiddlewareInterface>[] $classNameList */
|
||||
$classNameList = $this->metadata->get(['app', 'api', 'actionMiddlewareClassNameListMap', $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;
|
||||
}
|
||||
}
|
||||
66
application/Espo/Core/Api/ProcessData.php
Normal file
66
application/Espo/Core/Api/ProcessData.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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;
|
||||
|
||||
class ProcessData
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $routeParams
|
||||
*/
|
||||
public function __construct(
|
||||
private Route $route,
|
||||
private string $basePath,
|
||||
private array $routeParams
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return Route
|
||||
*/
|
||||
public function getRoute(): Route
|
||||
{
|
||||
return $this->route;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBasePath(): string
|
||||
{
|
||||
return $this->basePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRouteParams(): array
|
||||
{
|
||||
return $this->routeParams;
|
||||
}
|
||||
}
|
||||
128
application/Espo/Core/Api/Request.php
Normal file
128
application/Espo/Core/Api/Request.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?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 Psr\Http\Message\UriInterface;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Representation of an HTTP request.
|
||||
*/
|
||||
interface Request
|
||||
{
|
||||
/**
|
||||
* Whether a query parameter is set.
|
||||
*/
|
||||
public function hasQueryParam(string $name): bool;
|
||||
|
||||
/**
|
||||
* Get a query parameter.
|
||||
*/
|
||||
public function getQueryParam(string $name): ?string;
|
||||
|
||||
/**
|
||||
* Get all query parameters.
|
||||
*
|
||||
* @return array<string, string|array<scalar, mixed>>
|
||||
*/
|
||||
public function getQueryParams(): array;
|
||||
|
||||
/**
|
||||
* Whether a route parameter is set.
|
||||
*/
|
||||
public function hasRouteParam(string $name): bool;
|
||||
|
||||
/**
|
||||
* Get a route parameter.
|
||||
*/
|
||||
public function getRouteParam(string $name): ?string;
|
||||
|
||||
/**
|
||||
* Get all route parameters.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRouteParams(): array;
|
||||
|
||||
/**
|
||||
* Get a header value. Multiple values will be concatenated with a comma.
|
||||
*/
|
||||
public function getHeader(string $name): ?string;
|
||||
|
||||
/**
|
||||
* Whether a header is set.
|
||||
*/
|
||||
public function hasHeader(string $name): bool;
|
||||
|
||||
/**
|
||||
* Get a header values as an array.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderAsArray(string $name): array;
|
||||
|
||||
/**
|
||||
* Get a request method.
|
||||
*/
|
||||
public function getMethod(): string;
|
||||
|
||||
/**
|
||||
* Get Uri.
|
||||
*/
|
||||
public function getUri(): UriInterface;
|
||||
|
||||
/**
|
||||
* Get a relative path of a request (w/o base path).
|
||||
*/
|
||||
public function getResourcePath(): string;
|
||||
|
||||
/**
|
||||
* Get body contents.
|
||||
*/
|
||||
public function getBodyContents(): ?string;
|
||||
|
||||
/**
|
||||
* Get a parsed body. If JSON array is passed, then will be converted to `{"list": ARRAY}`.
|
||||
*/
|
||||
public function getParsedBody(): stdClass;
|
||||
|
||||
/**
|
||||
* Get a cookie param value.
|
||||
*/
|
||||
public function getCookieParam(string $name): ?string;
|
||||
|
||||
/**
|
||||
* Get a server param value.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getServerParam(string $name);
|
||||
}
|
||||
133
application/Espo/Core/Api/RequestNull.php
Normal file
133
application/Espo/Core/Api/RequestNull.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?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\Api\Request as ApiRequest;
|
||||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Slim\Psr7\Factory\UriFactory;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* An empty stub for Request.
|
||||
*/
|
||||
class RequestNull implements ApiRequest
|
||||
{
|
||||
public function hasQueryParam(string $name): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function getQueryParam(string $name): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getQueryParams(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function hasRouteParam(string $name): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getRouteParam(string $name): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getRouteParams(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getHeader(string $name): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function hasHeader(string $name): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderAsArray(string $name): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getUri(): UriInterface
|
||||
{
|
||||
return (new UriFactory())->createUri();
|
||||
}
|
||||
|
||||
public function getResourcePath(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getBodyContents(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getParsedBody(): stdClass
|
||||
{
|
||||
return (object) [];
|
||||
}
|
||||
|
||||
public function getCookieParam(string $name): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getServerParam(string $name)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
292
application/Espo/Core/Api/RequestWrapper.php
Normal file
292
application/Espo/Core/Api/RequestWrapper.php
Normal file
@@ -0,0 +1,292 @@
|
||||
<?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\Utils\Json;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Api\Request as ApiRequest;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface as Psr7Request;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Adapter for PSR-7 request interface.
|
||||
*/
|
||||
class RequestWrapper implements ApiRequest
|
||||
{
|
||||
private ?stdClass $parsedBody = null;
|
||||
|
||||
/**
|
||||
* @param array<string, string> $routeParams
|
||||
*/
|
||||
public function __construct(
|
||||
private Psr7Request $psr7Request,
|
||||
private string $basePath = '',
|
||||
private array $routeParams = []
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get a route or query parameter. Route params have a higher priority.
|
||||
*
|
||||
* @todo Don't support NULL $name.
|
||||
* @deprecated As of v6.0. Use getQueryParam & getRouteParam. Left for backward compatibility.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(?string $name = null)
|
||||
{
|
||||
if (is_null($name)) {
|
||||
return array_merge(
|
||||
$this->getQueryParams(),
|
||||
$this->routeParams
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->hasRouteParam($name)) {
|
||||
return $this->getRouteParam($name);
|
||||
}
|
||||
|
||||
return $this->psr7Request->getQueryParams()[$name] ?? null;
|
||||
}
|
||||
|
||||
public function hasRouteParam(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->routeParams);
|
||||
}
|
||||
|
||||
public function getRouteParam(string $name): ?string
|
||||
{
|
||||
return $this->routeParams[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRouteParams(): array
|
||||
{
|
||||
return $this->routeParams;
|
||||
}
|
||||
|
||||
public function hasQueryParam(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->psr7Request->getQueryParams());
|
||||
}
|
||||
|
||||
public function getQueryParam(string $name): ?string
|
||||
{
|
||||
$value = $this->psr7Request->getQueryParams()[$name] ?? null;
|
||||
|
||||
if (!is_string($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getQueryParams(): array
|
||||
{
|
||||
return $this->psr7Request->getQueryParams();
|
||||
}
|
||||
|
||||
public function getHeader(string $name): ?string
|
||||
{
|
||||
if (!$this->psr7Request->hasHeader($name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->psr7Request->getHeaderLine($name);
|
||||
}
|
||||
|
||||
public function hasHeader(string $name): bool
|
||||
{
|
||||
return $this->psr7Request->hasHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderAsArray(string $name): array
|
||||
{
|
||||
if (!$this->psr7Request->hasHeader($name)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->psr7Request->getHeader($name);
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->psr7Request->getMethod();
|
||||
}
|
||||
|
||||
public function getContentType(): ?string
|
||||
{
|
||||
if (!$this->hasHeader('Content-Type')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$contentType = explode(
|
||||
';',
|
||||
$this->psr7Request->getHeader('Content-Type')[0]
|
||||
)[0];
|
||||
|
||||
return strtolower($contentType);
|
||||
}
|
||||
|
||||
public function getBodyContents(): ?string
|
||||
{
|
||||
$contents = $this->psr7Request->getBody()->getContents();
|
||||
|
||||
$this->psr7Request->getBody()->rewind();
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function getParsedBody(): stdClass
|
||||
{
|
||||
if ($this->parsedBody === null) {
|
||||
$this->initParsedBody();
|
||||
}
|
||||
|
||||
if ($this->parsedBody === null) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
return Util::cloneObject($this->parsedBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function initParsedBody(): void
|
||||
{
|
||||
$contents = $this->getBodyContents();
|
||||
|
||||
$contentType = $this->getContentType();
|
||||
|
||||
if ($contentType === 'application/json' && $contents) {
|
||||
$parsedBody = Json::decode($contents);
|
||||
|
||||
if (is_array($parsedBody)) {
|
||||
$parsedBody = (object) [
|
||||
'list' => $parsedBody,
|
||||
];
|
||||
}
|
||||
|
||||
if (!$parsedBody instanceof stdClass) {
|
||||
throw new BadRequest("Body is not a JSON object.");
|
||||
}
|
||||
|
||||
$this->parsedBody = $parsedBody;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
in_array($contentType, ['application/x-www-form-urlencoded', 'multipart/form-data']) &&
|
||||
$contents
|
||||
) {
|
||||
$parsedBody = $this->psr7Request->getParsedBody();
|
||||
|
||||
if (is_array($parsedBody)) {
|
||||
$this->parsedBody = (object) $parsedBody;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($parsedBody instanceof stdClass) {
|
||||
$this->parsedBody = $parsedBody;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->parsedBody = (object) [];
|
||||
}
|
||||
|
||||
public function getCookieParam(string $name): ?string
|
||||
{
|
||||
$params = $this->psr7Request->getCookieParams();
|
||||
|
||||
return $params[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getServerParam(string $name)
|
||||
{
|
||||
$params = $this->psr7Request->getServerParams();
|
||||
|
||||
return $params[$name] ?? null;
|
||||
}
|
||||
|
||||
public function getUri(): UriInterface
|
||||
{
|
||||
return $this->psr7Request->getUri();
|
||||
}
|
||||
|
||||
public function getResourcePath(): string
|
||||
{
|
||||
$path = $this->psr7Request->getUri()->getPath();
|
||||
|
||||
return substr($path, strlen($this->basePath));
|
||||
}
|
||||
|
||||
public function isGet(): bool
|
||||
{
|
||||
return $this->getMethod() === Method::GET;
|
||||
}
|
||||
|
||||
public function isPut(): bool
|
||||
{
|
||||
return $this->getMethod() === Method::PUT;
|
||||
}
|
||||
|
||||
public function isPost(): bool
|
||||
{
|
||||
return $this->getMethod() === Method::POST;
|
||||
}
|
||||
|
||||
public function isPatch(): bool
|
||||
{
|
||||
return $this->getMethod() === Method::PATCH;
|
||||
}
|
||||
|
||||
public function isDelete(): bool
|
||||
{
|
||||
return $this->getMethod() === Method::DELETE;
|
||||
}
|
||||
}
|
||||
102
application/Espo/Core/Api/Response.php
Normal file
102
application/Espo/Core/Api/Response.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?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 Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Representation of an HTTP response. An instance is mutable.
|
||||
*/
|
||||
interface Response
|
||||
{
|
||||
/**
|
||||
* Get a status code.
|
||||
*/
|
||||
public function getStatusCode(): int;
|
||||
|
||||
/**
|
||||
* Get a status reason phrase.
|
||||
*/
|
||||
public function getReasonPhrase(): string;
|
||||
|
||||
/**
|
||||
* Set a status code.
|
||||
*/
|
||||
public function setStatus(int $code, ?string $reason = null): self;
|
||||
|
||||
/**
|
||||
* Set a specific header.
|
||||
*/
|
||||
public function setHeader(string $name, string $value): self;
|
||||
|
||||
/**
|
||||
* Add a specific header.
|
||||
*/
|
||||
public function addHeader(string $name, string $value): self;
|
||||
|
||||
/**
|
||||
* Get a header value.
|
||||
*/
|
||||
public function getHeader(string $name): ?string;
|
||||
|
||||
/**
|
||||
* Whether a header is set.
|
||||
*/
|
||||
public function hasHeader(string $name): bool;
|
||||
|
||||
/**
|
||||
* Get all set header names.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderNames(): array;
|
||||
|
||||
/**
|
||||
* Get a header values as an array.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderAsArray(string $name): array;
|
||||
|
||||
/**
|
||||
* Write a body.
|
||||
*/
|
||||
public function writeBody(string $string): self;
|
||||
|
||||
/**
|
||||
* Set a body.
|
||||
*/
|
||||
public function setBody(StreamInterface $body): self;
|
||||
|
||||
/**
|
||||
* Get a body.
|
||||
*/
|
||||
public function getBody(): StreamInterface;
|
||||
}
|
||||
59
application/Espo/Core/Api/ResponseComposer.php
Normal file
59
application/Espo/Core/Api/ResponseComposer.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?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 Slim\Psr7\Factory\ResponseFactory;
|
||||
use Espo\Core\Utils\Json;
|
||||
use stdClass;
|
||||
|
||||
class ResponseComposer
|
||||
{
|
||||
/**
|
||||
* Compose a JSON response.
|
||||
*
|
||||
* @param array<string|int, mixed>|stdClass|scalar|null $data A data to encode.
|
||||
*/
|
||||
public static function json(mixed $data): Response
|
||||
{
|
||||
return self::empty()
|
||||
->writeBody(Json::encode($data))
|
||||
->setHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose an empty response.
|
||||
*/
|
||||
public static function empty(): Response
|
||||
{
|
||||
$psr7Response = (new ResponseFactory())->createResponse();
|
||||
|
||||
return new ResponseWrapper($psr7Response);
|
||||
}
|
||||
}
|
||||
136
application/Espo/Core/Api/ResponseWrapper.php
Normal file
136
application/Espo/Core/Api/ResponseWrapper.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?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 Psr\Http\Message\ResponseInterface as Psr7Response;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
use Espo\Core\Api\Response as ApiResponse;
|
||||
|
||||
/**
|
||||
* Adapter for PSR-7 response interface.
|
||||
*/
|
||||
class ResponseWrapper implements ApiResponse
|
||||
{
|
||||
public function __construct(private Psr7Response $psr7Response)
|
||||
{
|
||||
// Slim adds Authorization header. It's not needed.
|
||||
$this->psr7Response = $this->psr7Response->withoutHeader('Authorization');
|
||||
}
|
||||
|
||||
public function setStatus(int $code, ?string $reason = null): Response
|
||||
{
|
||||
$this->psr7Response = $this->psr7Response->withStatus($code, $reason ?? '');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->psr7Response->getStatusCode();
|
||||
}
|
||||
|
||||
public function getReasonPhrase(): string
|
||||
{
|
||||
return $this->psr7Response->getReasonPhrase();
|
||||
}
|
||||
|
||||
public function setHeader(string $name, string $value): Response
|
||||
{
|
||||
$this->psr7Response = $this->psr7Response->withHeader($name, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addHeader(string $name, string $value): Response
|
||||
{
|
||||
$this->psr7Response = $this->psr7Response->withAddedHeader($name, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHeader(string $name): ?string
|
||||
{
|
||||
if (!$this->psr7Response->hasHeader($name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->psr7Response->getHeaderLine($name);
|
||||
}
|
||||
|
||||
public function hasHeader(string $name): bool
|
||||
{
|
||||
return $this->psr7Response->hasHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderAsArray(string $name): array
|
||||
{
|
||||
if (!$this->psr7Response->hasHeader($name)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->psr7Response->getHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderNames(): array
|
||||
{
|
||||
return array_keys($this->psr7Response->getHeaders());
|
||||
}
|
||||
|
||||
public function writeBody(string $string): Response
|
||||
{
|
||||
$this->psr7Response->getBody()->write($string);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setBody(StreamInterface $body): Response
|
||||
{
|
||||
$this->psr7Response = $this->psr7Response->withBody($body);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBody(): StreamInterface
|
||||
{
|
||||
return $this->psr7Response->getBody();
|
||||
}
|
||||
|
||||
public function toPsr7(): Psr7Response
|
||||
{
|
||||
return $this->psr7Response;
|
||||
}
|
||||
}
|
||||
92
application/Espo/Core/Api/Route.php
Normal file
92
application/Espo/Core/Api/Route.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?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;
|
||||
|
||||
class Route
|
||||
{
|
||||
private string $method;
|
||||
|
||||
/**
|
||||
* @param array<string, string> $params
|
||||
* @param ?class-string<Action> $actionClassName
|
||||
*/
|
||||
public function __construct(
|
||||
string $method,
|
||||
private string $route,
|
||||
private string $adjustedRoute,
|
||||
private array $params,
|
||||
private bool $noAuth,
|
||||
private ?string $actionClassName
|
||||
) {
|
||||
$this->method = strtoupper($method);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?class-string<Action>
|
||||
*/
|
||||
public function getActionClassName(): ?string
|
||||
{
|
||||
return $this->actionClassName;
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a route.
|
||||
*/
|
||||
public function getRoute(): string
|
||||
{
|
||||
return $this->route;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an adjusted route for FastRoute.
|
||||
*/
|
||||
public function getAdjustedRoute(): string
|
||||
{
|
||||
return $this->adjustedRoute;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getParams(): array
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
public function noAuth(): bool
|
||||
{
|
||||
return $this->noAuth;
|
||||
}
|
||||
}
|
||||
76
application/Espo/Core/Api/Route/RouteParamsFetcher.php
Normal file
76
application/Espo/Core/Api/Route/RouteParamsFetcher.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?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\Route;
|
||||
|
||||
use Espo\Core\Api\Route;
|
||||
|
||||
class RouteParamsFetcher
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function fetch(Route $item, array $args): array
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$routeParams = $item->getParams();
|
||||
|
||||
$setKeyList = [];
|
||||
|
||||
foreach (array_keys($routeParams) as $key) {
|
||||
$value = $routeParams[$key];
|
||||
|
||||
$paramName = $key;
|
||||
|
||||
if ($value[0] === ':') {
|
||||
$realKey = substr($value, 1);
|
||||
|
||||
$params[$paramName] = $args[$realKey];
|
||||
|
||||
$setKeyList[] = $realKey;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$params[$paramName] = $value;
|
||||
}
|
||||
|
||||
foreach ($args as $key => $value) {
|
||||
if (in_array($key, $setKeyList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$params[$key] = $value;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
275
application/Espo/Core/Api/RouteProcessor.php
Normal file
275
application/Espo/Core/Api/RouteProcessor.php
Normal file
@@ -0,0 +1,275 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
154
application/Espo/Core/Api/Starter.php
Normal file
154
application/Espo/Core/Api/Starter.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?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\Api\Route\RouteParamsFetcher;
|
||||
use Espo\Core\Utils\Config\SystemConfig;
|
||||
use Espo\Core\Utils\Route as RouteUtil;
|
||||
use Espo\Core\Utils\Log;
|
||||
|
||||
use Slim\App as SlimApp;
|
||||
use Slim\Exception\HttpBadRequestException;
|
||||
use Slim\Factory\AppFactory as SlimAppFactory;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Psr7Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Psr7Request;
|
||||
|
||||
/**
|
||||
* API request processing entry point.
|
||||
*/
|
||||
class Starter
|
||||
{
|
||||
private string $routeCacheFile = 'data/cache/application/slim-routes.php';
|
||||
|
||||
public function __construct(
|
||||
private RouteProcessor $routeProcessor,
|
||||
private RouteUtil $routeUtil,
|
||||
private RouteParamsFetcher $routeParamsFetcher,
|
||||
private MiddlewareProvider $middlewareProvider,
|
||||
private Log $log,
|
||||
private SystemConfig $systemConfig,
|
||||
?string $routeCacheFile = null
|
||||
) {
|
||||
$this->routeCacheFile = $routeCacheFile ?? $this->routeCacheFile;
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
$slim = SlimAppFactory::create();
|
||||
|
||||
if (RouteUtil::isBadUri()) {
|
||||
$this->processError($slim);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->systemConfig->useCache()) {
|
||||
$slim->getRouteCollector()->setCacheFile($this->routeCacheFile);
|
||||
}
|
||||
|
||||
$slim->setBasePath(RouteUtil::detectBasePath());
|
||||
$this->addGlobalMiddlewares($slim);
|
||||
$slim->addRoutingMiddleware();
|
||||
$this->addRoutes($slim);
|
||||
$slim->addErrorMiddleware(false, true, true, $this->log);
|
||||
|
||||
$slim->run();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SlimApp<ContainerInterface|null> $slim
|
||||
*/
|
||||
private function addGlobalMiddlewares(SlimApp $slim): void
|
||||
{
|
||||
foreach ($this->middlewareProvider->getGlobalMiddlewareList() as $middleware) {
|
||||
$slim->add($middleware);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SlimApp<ContainerInterface|null> $slim
|
||||
*/
|
||||
private function addRoutes(SlimApp $slim): void
|
||||
{
|
||||
$routeList = $this->routeUtil->getFullList();
|
||||
|
||||
foreach ($routeList as $item) {
|
||||
$this->addRoute($slim, $item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SlimApp<ContainerInterface|null> $slim
|
||||
*/
|
||||
private function addRoute(SlimApp $slim, Route $item): void
|
||||
{
|
||||
$slimRoute = $slim->map(
|
||||
[$item->getMethod()],
|
||||
$item->getAdjustedRoute(),
|
||||
function (Psr7Request $request, Psr7Response $response, array $args) use ($slim, $item) {
|
||||
$routeParams = $this->routeParamsFetcher->fetch($item, $args);
|
||||
|
||||
$processData = new ProcessData(
|
||||
route: $item,
|
||||
basePath: $slim->getBasePath(),
|
||||
routeParams: $routeParams,
|
||||
);
|
||||
|
||||
return $this->routeProcessor->process($processData, $request, $response);
|
||||
}
|
||||
);
|
||||
|
||||
$middlewareList = $this->middlewareProvider->getRouteMiddlewareList($item);
|
||||
|
||||
foreach ($middlewareList as $middleware) {
|
||||
$slimRoute->addMiddleware($middleware);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SlimApp<ContainerInterface|null> $slim
|
||||
*/
|
||||
private function processError(SlimApp $slim): void
|
||||
{
|
||||
$slim->add(function (Psr7Request $request): Psr7Response {
|
||||
throw new HttpBadRequestException($request, 'Malformed request path.');
|
||||
});
|
||||
|
||||
$slim->addErrorMiddleware(
|
||||
displayErrorDetails: false,
|
||||
logErrors: false,
|
||||
logErrorDetails: false,
|
||||
);
|
||||
|
||||
$slim->run();
|
||||
}
|
||||
}
|
||||
85
application/Espo/Core/Api/Util.php
Normal file
85
application/Espo/Core/Api/Util.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?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\Utils\Config;
|
||||
use stdClass;
|
||||
|
||||
class Util
|
||||
{
|
||||
public function __construct(private Config $config) {}
|
||||
|
||||
public static function cloneObject(stdClass $source): stdClass
|
||||
{
|
||||
$cloned = (object) [];
|
||||
|
||||
foreach (get_object_vars($source) as $k => $v) {
|
||||
$cloned->$k = self::cloneObjectItem($v);
|
||||
}
|
||||
|
||||
return $cloned;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $item
|
||||
* @return mixed
|
||||
*/
|
||||
private static function cloneObjectItem($item)
|
||||
{
|
||||
if (is_array($item)) {
|
||||
$cloned = [];
|
||||
|
||||
foreach ($item as $v) {
|
||||
$cloned[] = self::cloneObjectItem($v);
|
||||
}
|
||||
|
||||
return $cloned;
|
||||
}
|
||||
|
||||
if ($item instanceof stdClass) {
|
||||
return self::cloneObject($item);
|
||||
}
|
||||
|
||||
if (is_object($item)) {
|
||||
return clone $item;
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
public function obtainIpFromRequest(Request $request): ?string
|
||||
{
|
||||
// Do not add support of any more parameters here.
|
||||
|
||||
$param = $this->config->get('ipAddressServerParam') ?? 'REMOTE_ADDR';
|
||||
|
||||
return $request->getServerParam($param);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user