api action

This commit is contained in:
Yuri Kuznetsov
2023-02-17 20:17:18 +02:00
parent 1018bfd4d4
commit ace2dc802a
19 changed files with 612 additions and 363 deletions

View File

@@ -0,0 +1,53 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Api;
use Espo\Core\Exceptions\BadRequest;
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.
* @param Response $response A response. Passed empty, to be written in the method.
* @throws BadRequest
* @throws Forbidden
* @throws NotFound
* @throws Error
*/
public function process(Request $request, Response $response): void;
}

View File

@@ -68,7 +68,7 @@ class ActionHandler implements RequestHandlerInterface
$this->afterProceed($responseWrapped);
return $responseWrapped->getResponse();
return $responseWrapped->toPsr7();
}
private function beforeProceed(): void

View File

@@ -31,6 +31,7 @@ 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;
@@ -44,10 +45,12 @@ use LogicException;
/**
* Processes requests. Handles authentication. Obtains a controller name, action, body from a request.
* Then passes them to the action processor.
* Then passes them to the action processor or processes an action class.
*/
class RequestProcessor
{
private const DEFAULT_CONTENT_TYPE = 'application/json';
public function __construct(
private AuthenticationFactory $authenticationFactory,
private AuthBuilderFactory $authBuilderFactory,
@@ -56,7 +59,8 @@ class RequestProcessor
private Log $log,
private ApplicationUser $applicationUser,
private ControllerActionProcessor $actionProcessor,
private MiddlewareProvider $middlewareProvider
private MiddlewareProvider $middlewareProvider,
private InjectableFactory $injectableFactory
) {}
public function process(
@@ -84,7 +88,7 @@ class RequestProcessor
$processData->getRoute()->getAdjustedRoute()
);
return $responseWrapped->getResponse();
return $responseWrapped->toPsr7();
}
}
@@ -109,7 +113,7 @@ class RequestProcessor
$authResult = $apiAuth->process($request, $response);
if (!$authResult->isResolved()) {
return $response->getResponse();
return $response->toPsr7();
}
if ($authResult->isResolvedUseNoAuth()) {
@@ -118,7 +122,7 @@ class RequestProcessor
ob_start();
$response = $this->proceed($processData, $psrRequest, $response);
$response = $this->processAfterAuth($processData, $psrRequest, $request, $response);
ob_clean();
@@ -128,12 +132,19 @@ class RequestProcessor
/**
* @throws BadRequest
*/
private function proceed(
private function processAfterAuth(
ProcessData $processData,
Psr7Request $request,
RequestWrapper $requestWrapped,
ResponseWrapper $responseWrapped
): Psr7Response {
$actionClassName = $processData->getRoute()->getActionClassName();
if ($actionClassName) {
return $this->processAction($actionClassName, $requestWrapped, $responseWrapped);
}
$controller = $this->getControllerName($processData);
$action = $processData->getRouteParams()['action'] ?? null;
$method = $request->getMethod();
@@ -163,6 +174,29 @@ class RequestProcessor
return $dispatcher->handle($request);
}
/**
* @param class-string<Action> $actionClassName
*/
private function processAction(
string $actionClassName,
RequestWrapper $requestWrapped,
ResponseWrapper $responseWrapped
): Psr7Response {
/** @var Action $action */
$action = $this->injectableFactory->create($actionClassName);
$action->process($requestWrapped, $responseWrapped);
$response = $responseWrapped->toPsr7();
if (!$response->getHeader('Content-Type')) {
$response = $response->withHeader('Content-Type', self::DEFAULT_CONTENT_TYPE);
}
return $response;
}
private function getControllerName(ProcessData $processData): string
{
$controllerName = $processData->getRouteParams()['controller'] ?? null;

View File

@@ -43,21 +43,16 @@ use stdClass;
*/
class RequestWrapper implements ApiRequest
{
private Psr7Request $request;
private string $basePath;
private ?stdClass $parsedBody = null;
/** @var array<string,string> */
private array $routeParams;
/**
* @param array<string, string> $routeParams
*/
public function __construct(Psr7Request $request, string $basePath = '', array $routeParams = [])
{
$this->request = $request;
$this->basePath = $basePath;
$this->routeParams = $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.
@@ -80,7 +75,7 @@ class RequestWrapper implements ApiRequest
return $this->getRouteParam($name);
}
return $this->request->getQueryParams()[$name] ?? null;
return $this->psr7Request->getQueryParams()[$name] ?? null;
}
public function hasRouteParam(string $name): bool
@@ -94,7 +89,7 @@ class RequestWrapper implements ApiRequest
}
/**
* @return array<string,string>
* @return array<string, string>
*/
public function getRouteParams(): array
{
@@ -103,12 +98,12 @@ class RequestWrapper implements ApiRequest
public function hasQueryParam(string $name): bool
{
return array_key_exists($name, $this->request->getQueryParams());
return array_key_exists($name, $this->psr7Request->getQueryParams());
}
public function getQueryParam(string $name): ?string
{
$value = $this->request->getQueryParams()[$name] ?? null;
$value = $this->psr7Request->getQueryParams()[$name] ?? null;
if (!is_string($value)) {
return null;
@@ -119,21 +114,21 @@ class RequestWrapper implements ApiRequest
public function getQueryParams(): array
{
return $this->request->getQueryParams();
return $this->psr7Request->getQueryParams();
}
public function getHeader(string $name): ?string
{
if (!$this->request->hasHeader($name)) {
if (!$this->psr7Request->hasHeader($name)) {
return null;
}
return $this->request->getHeaderLine($name);
return $this->psr7Request->getHeaderLine($name);
}
public function hasHeader(string $name): bool
{
return $this->request->hasHeader($name);
return $this->psr7Request->hasHeader($name);
}
/**
@@ -141,16 +136,16 @@ class RequestWrapper implements ApiRequest
*/
public function getHeaderAsArray(string $name): array
{
if (!$this->request->hasHeader($name)) {
if (!$this->psr7Request->hasHeader($name)) {
return [];
}
return $this->request->getHeader($name);
return $this->psr7Request->getHeader($name);
}
public function getMethod(): string
{
return $this->request->getMethod();
return $this->psr7Request->getMethod();
}
public function getContentType(): ?string
@@ -161,7 +156,7 @@ class RequestWrapper implements ApiRequest
$contentType = explode(
';',
$this->request->getHeader('Content-Type')[0]
$this->psr7Request->getHeader('Content-Type')[0]
)[0];
return strtolower($contentType);
@@ -169,9 +164,9 @@ class RequestWrapper implements ApiRequest
public function getBodyContents(): ?string
{
$contents = $this->request->getBody()->getContents();
$contents = $this->psr7Request->getBody()->getContents();
$this->request->getBody()->rewind();
$this->psr7Request->getBody()->rewind();
return $contents;
}
@@ -223,7 +218,7 @@ class RequestWrapper implements ApiRequest
in_array($contentType, ['application/x-www-form-urlencoded', 'multipart/form-data']) &&
$contents
) {
$parsedBody = $this->request->getParsedBody();
$parsedBody = $this->psr7Request->getParsedBody();
if (is_array($parsedBody)) {
$this->parsedBody = (object) $parsedBody;
@@ -243,7 +238,7 @@ class RequestWrapper implements ApiRequest
public function getCookieParam(string $name): ?string
{
$params = $this->request->getCookieParams();
$params = $this->psr7Request->getCookieParams();
return $params[$name] ?? null;
}
@@ -253,19 +248,19 @@ class RequestWrapper implements ApiRequest
*/
public function getServerParam(string $name)
{
$params = $this->request->getServerParams();
$params = $this->psr7Request->getServerParams();
return $params[$name] ?? null;
}
public function getUri(): UriInterface
{
return $this->request->getUri();
return $this->psr7Request->getUri();
}
public function getResourcePath(): string
{
$path = $this->request->getUri()->getPath();
$path = $this->psr7Request->getUri()->getPath();
return substr($path, strlen($this->basePath));
}

View File

@@ -29,6 +29,7 @@
namespace Espo\Core\Api;
use Psr\Http\Message\ResponseInterface as Psr7Response;
use Psr\Http\Message\StreamInterface;
/**

View File

@@ -39,45 +39,45 @@ use Espo\Core\Api\Response as ApiResponse;
*/
class ResponseWrapper implements ApiResponse
{
public function __construct(private Psr7Response $response)
public function __construct(private Psr7Response $psr7Response)
{
// Slim adds Authorization header. It's not needed.
$this->response = $this->response->withoutHeader('Authorization');
$this->psr7Response = $this->psr7Response->withoutHeader('Authorization');
}
public function setStatus(int $code, ?string $reason = null): Response
{
$this->response = $this->response->withStatus($code, $reason ?? '');
$this->psr7Response = $this->psr7Response->withStatus($code, $reason ?? '');
return $this;
}
public function setHeader(string $name, string $value): Response
{
$this->response = $this->response->withHeader($name, $value);
$this->psr7Response = $this->psr7Response->withHeader($name, $value);
return $this;
}
public function addHeader(string $name, string $value): Response
{
$this->response = $this->response->withAddedHeader($name, $value);
$this->psr7Response = $this->psr7Response->withAddedHeader($name, $value);
return $this;
}
public function getHeader(string $name): ?string
{
if (!$this->response->hasHeader($name)) {
if (!$this->psr7Response->hasHeader($name)) {
return null;
}
return $this->response->getHeaderLine($name);
return $this->psr7Response->getHeaderLine($name);
}
public function hasHeader(string $name): bool
{
return $this->response->hasHeader($name);
return $this->psr7Response->hasHeader($name);
}
/**
@@ -85,29 +85,29 @@ class ResponseWrapper implements ApiResponse
*/
public function getHeaderAsArray(string $name): array
{
if (!$this->response->hasHeader($name)) {
if (!$this->psr7Response->hasHeader($name)) {
return [];
}
return $this->response->getHeader($name);
return $this->psr7Response->getHeader($name);
}
public function writeBody(string $string): Response
{
$this->response->getBody()->write($string);
$this->psr7Response->getBody()->write($string);
return $this;
}
public function setBody(StreamInterface $body): Response
{
$this->response = $this->response->withBody($body);
$this->psr7Response = $this->psr7Response->withBody($body);
return $this;
}
public function getResponse(): Psr7Response
public function toPsr7(): Psr7Response
{
return $this->response;
return $this->psr7Response;
}
}

View File

@@ -35,17 +35,27 @@ class Route
/**
* @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 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;

View File

@@ -100,6 +100,6 @@ class PortalClient implements RunnerParameterized
{
$this->errorOutput->processWithBodyPrinting($request, $response, $exception);
(new ResponseEmitter())->emit($response->getResponse());
(new ResponseEmitter())->emit($response->toPsr7());
}
}

View File

@@ -95,7 +95,7 @@ class Starter
catch (NotFound $exception) {
$this->errorOutput->processWithBodyPrinting($requestWrapped, $responseWrapped, $exception);
(new ResponseEmitter())->emit($responseWrapped->getResponse());
(new ResponseEmitter())->emit($responseWrapped->toPsr7());
return;
}
@@ -117,7 +117,7 @@ class Starter
$authRequired
);
(new ResponseEmitter())->emit($responseWrapped->getResponse());
(new ResponseEmitter())->emit($responseWrapped->toPsr7());
}
private function processRequest(

View File

@@ -54,7 +54,8 @@ class Route extends BaseRoute
$route->getRoute(),
$path,
$route->getParams(),
$route->noAuth()
$route->noAuth(),
$route->getActionClassName()
);
$newRouteList[] = $newRoute;

View File

@@ -149,7 +149,7 @@ class ClientManager
$this->writeHeaders($response);
$response->writeBody($body);
(new ResponseEmitter())->emit($response->getResponse());
(new ResponseEmitter())->emit($response->toPsr7());
}
/**

View File

@@ -40,11 +40,12 @@ use Espo\Core\Utils\Resource\PathProvider;
* method: string,
* noAuth?: bool,
* params?: array<string, mixed>,
* actionClassName: ?class-string<\Espo\Core\Api\Action>
* }
*/
class Route
{
/** @var ?(RouteArrayShape[]) */
/** @var ?RouteArrayShape[] */
private $data = null;
private string $cacheKey = 'routes';
private string $routesFileName = 'routes.json';
@@ -77,7 +78,8 @@ class Route
$item['route'],
$item['adjustedRoute'],
$item['params'] ?? [],
$item['noAuth'] ?? false
$item['noAuth'] ?? false,
$item['actionClassName'] ?? null
);
},
$this->data
@@ -242,8 +244,8 @@ class Route
}
/**
* @param array<string, mixed> $newRoute
* @param array<int, array<string, mixed>> $routeList
* @param RouteArrayShape $newRoute
* @param array<int, RouteArrayShape> $routeList
*/
static private function isRouteInList(array $newRoute, array $routeList): bool
{

View File

@@ -33,184 +33,23 @@ use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Api\Request;
use Espo\Core\Acl;
use Espo\Core\Field\DateTime;
use Espo\Core\Record\SearchParamsFetcher;
use Espo\Modules\Crm\Tools\Activities\FetchParams as ActivitiesFetchParams;
use Espo\Modules\Crm\Tools\Calendar\FetchParams;
use Espo\Modules\Crm\Tools\Activities\Service as Service;
use Espo\Modules\Crm\Tools\Calendar\Item as CalendarItem;
use Espo\Modules\Crm\Tools\Calendar\Service as CalendarService;
use Espo\Entities\User;
use stdClass;
use Exception;
class Activities
{
private const MAX_CALENDAR_RANGE = 123;
private User $user;
private Acl $acl;
private SearchParamsFetcher $searchParamsFetcher;
private Service $service;
private CalendarService $calendarService;
public function __construct(
User $user,
Acl $acl,
SearchParamsFetcher $searchParamsFetcher,
Service $service,
CalendarService $calendarService
) {
$this->user = $user;
$this->acl = $acl;
$this->searchParamsFetcher = $searchParamsFetcher;
$this->service = $service;
$this->calendarService = $calendarService;
}
/**
* @return array<int,stdClass>
* @throws Forbidden
* @throws BadRequest
* @throws Exception
*/
public function getActionListCalendarEvents(Request $request): array
{
if (!$this->acl->check('Calendar')) {
throw new Forbidden();
}
$from = $request->getQueryParam('from');
$to = $request->getQueryParam('to');
$isAgenda = $request->getQueryParam('agenda') === 'true';
if (empty($from) || empty($to)) {
throw new BadRequest();
}
if (strtotime($to) - strtotime($from) > self::MAX_CALENDAR_RANGE * 24 * 3600) {
throw new Forbidden('Too long range.');
}
$scopeList = null;
if ($request->getQueryParam('scopeList') !== null) {
$scopeList = explode(',', $request->getQueryParam('scopeList'));
}
$userId = $request->getQueryParam('userId');
$userIdList = $request->getQueryParam('userIdList');
$teamIdList = $request->getQueryParam('teamIdList');
$fetchParams = FetchParams
::create(
DateTime::fromString($from),
DateTime::fromString($to)
)
->withScopeList($scopeList);
if ($teamIdList) {
$teamIdList = explode(',', $teamIdList);
return self::itemListToRaw(
$this->calendarService->fetchForTeams($teamIdList, $fetchParams)
);
}
if ($userIdList) {
$userIdList = explode(',', $userIdList);
return self::itemListToRaw(
$this->calendarService->fetchForUsers($userIdList, $fetchParams)
);
}
if (!$userId) {
$userId = $this->user->getId();
}
$fetchParams = $fetchParams
->withIsAgenda($isAgenda)
->withWorkingTimeRanges();
return self::itemListToRaw(
$this->calendarService->fetch($userId, $fetchParams)
);
}
/**
* @param CalendarItem[] $itemList
* @return stdClass[]
*/
private static function itemListToRaw(array $itemList): array
{
return array_map(fn (CalendarItem $item) => $item->getRaw(), $itemList);
}
/**
* @throws BadRequest
* @throws Forbidden
* @throws Exception
*/
public function getActionGetTimeline(Request $request): stdClass
{
if (!$this->acl->check('Calendar')) {
throw new Forbidden();
}
$from = $request->getQueryParam('from');
$to = $request->getQueryParam('to');
if (empty($from) || empty($to)) {
throw new BadRequest();
}
if (strtotime($to) - strtotime($from) > self::MAX_CALENDAR_RANGE * 24 * 3600) {
throw new Forbidden('Too long range.');
}
$scopeList = null;
if ($request->getQueryParam('scopeList') !== null) {
$scopeList = explode(',', $request->getQueryParam('scopeList'));
}
$userId = $request->getQueryParam('userId');
$userIdList = $request->getQueryParam('userIdList');
if ($userIdList) {
$userIdList = explode(',', $userIdList);
}
else {
$userIdList = [];
}
if ($userId) {
$userIdList[] = $userId;
}
$fetchParams = FetchParams
::create(
DateTime::fromString($from . ':00'),
DateTime::fromString($to . ':00')
)
->withScopeList($scopeList);
$map = $this->calendarService->fetchTimelineForUsers($userIdList, $fetchParams);
$result = (object) [];
foreach ($map as $userId => $itemList) {
$result->$userId = self::itemListToRaw($itemList);
}
return $result;
}
private User $user,
private Acl $acl,
private SearchParamsFetcher $searchParamsFetcher,
private Service $service
) {}
/**
* @throws Forbidden
@@ -378,38 +217,4 @@ class Activities
'list' => $result->getValueMapList(),
];
}
/**
* @throws BadRequest
* @throws Forbidden
* @throws Error
*/
public function getActionBusyRanges(Request $request): stdClass
{
$from = $request->getQueryParam('from');
$to = $request->getQueryParam('to');
$userIdListString = $request->getQueryParam('userIdList');
if (!$from || !$to || !$userIdListString) {
throw new BadRequest();
}
$userIdList = explode(',', $userIdListString);
$map = $this->calendarService->fetchBusyRangesForUsers(
$userIdList,
DateTime::fromString($from),
DateTime::fromString($to),
$request->getQueryParam('entityType'),
$request->getQueryParam('entityId')
);
$result = (object) [];
foreach ($map as $userId => $itemList) {
$result->$userId = self::itemListToRaw($itemList);
}
return $result;
}
}

View File

@@ -13,18 +13,17 @@
{
"route": "/Activities",
"method": "get",
"params": {
"controller": "Activities",
"action": "listCalendarEvents"
}
"actionClassName": "Espo\\Modules\\Crm\\Tools\\Calendar\\Api\\GetCalendar"
},
{
"route": "/Timeline",
"method": "get",
"params": {
"controller": "Activities",
"action": "getTimeline"
}
"actionClassName": "Espo\\Modules\\Crm\\Tools\\Calendar\\Api\\GetTimeline"
},
{
"route": "/Timeline/busyRanges",
"method": "get",
"actionClassName": "Espo\\Modules\\Crm\\Tools\\Calendar\\Api\\GetBusyRanges"
},
{
"route": "/Activities/:scope/:id/:name/list/:entityType",

View File

@@ -0,0 +1,87 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Modules\Crm\Tools\Calendar\Api;
use Espo\Core\Api\Action;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Field\DateTime;
use Espo\Core\Utils\Json;
use Espo\Modules\Crm\Tools\Calendar\Item as CalendarItem;
use Espo\Modules\Crm\Tools\Calendar\Service;
use stdClass;
/**
* Busy-ranges.
*/
class GetBusyRanges implements Action
{
public function __construct(private Service $calendarService) {}
public function process(Request $request, Response $response): void
{
$from = $request->getQueryParam('from');
$to = $request->getQueryParam('to');
$userIdListString = $request->getQueryParam('userIdList');
if (!$from || !$to || !$userIdListString) {
throw new BadRequest();
}
$userIdList = explode(',', $userIdListString);
$map = $this->calendarService->fetchBusyRangesForUsers(
$userIdList,
DateTime::fromString($from),
DateTime::fromString($to),
$request->getQueryParam('entityType'),
$request->getQueryParam('entityId')
);
$result = (object) [];
foreach ($map as $userId => $itemList) {
$result->$userId = self::itemListToRaw($itemList);
}
$response->writeBody(Json::encode($result));
}
/**
* @param CalendarItem[] $itemList
* @return stdClass[]
*/
private static function itemListToRaw(array $itemList): array
{
return array_map(fn (CalendarItem $item) => $item->getRaw(), $itemList);
}
}

View File

@@ -0,0 +1,142 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Modules\Crm\Tools\Calendar\Api;
use Espo\Core\Api\Action;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Acl;
use Espo\Core\Field\DateTime;
use Espo\Core\Utils\Json;
use Espo\Entities\User;
use Espo\Modules\Crm\Tools\Calendar\FetchParams;
use Espo\Modules\Crm\Tools\Calendar\Item as CalendarItem;
use Espo\Modules\Crm\Tools\Calendar\Service;
use stdClass;
/**
* Calendar events.
*/
class GetCalendar implements Action
{
private const MAX_CALENDAR_RANGE = 123;
public function __construct(
private Service $calendarService,
private Acl $acl,
private User $user
) {}
public function process(Request $request, Response $response): void
{
if (!$this->acl->check('Calendar')) {
throw new Forbidden();
}
$from = $request->getQueryParam('from');
$to = $request->getQueryParam('to');
$isAgenda = $request->getQueryParam('agenda') === 'true';
if (empty($from) || empty($to)) {
throw new BadRequest();
}
if (strtotime($to) - strtotime($from) > self::MAX_CALENDAR_RANGE * 24 * 3600) {
throw new Forbidden('Too long range.');
}
$scopeList = null;
if ($request->getQueryParam('scopeList') !== null) {
$scopeList = explode(',', $request->getQueryParam('scopeList'));
}
$userId = $request->getQueryParam('userId');
$userIdList = $request->getQueryParam('userIdList');
$teamIdList = $request->getQueryParam('teamIdList');
$fetchParams = FetchParams
::create(
DateTime::fromString($from),
DateTime::fromString($to)
)
->withScopeList($scopeList);
if ($teamIdList) {
$teamIdList = explode(',', $teamIdList);
$raw = self::itemListToRaw(
$this->calendarService->fetchForTeams($teamIdList, $fetchParams)
);
$response->writeBody(Json::encode($raw));
return;
}
if ($userIdList) {
$userIdList = explode(',', $userIdList);
$raw = self::itemListToRaw(
$this->calendarService->fetchForUsers($userIdList, $fetchParams)
);
$response->writeBody(Json::encode($raw));
return;
}
if (!$userId) {
$userId = $this->user->getId();
}
$fetchParams = $fetchParams
->withIsAgenda($isAgenda)
->withWorkingTimeRanges();
$raw = self::itemListToRaw(
$this->calendarService->fetch($userId, $fetchParams)
);
$response->writeBody(Json::encode($raw));
}
/**
* @param CalendarItem[] $itemList
* @return stdClass[]
*/
private static function itemListToRaw(array $itemList): array
{
return array_map(fn (CalendarItem $item) => $item->getRaw(), $itemList);
}
}

View File

@@ -0,0 +1,116 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Modules\Crm\Tools\Calendar\Api;
use Espo\Core\Api\Action;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Acl;
use Espo\Core\Field\DateTime;
use Espo\Core\Utils\Json;
use Espo\Modules\Crm\Tools\Calendar\FetchParams;
use Espo\Modules\Crm\Tools\Calendar\Item as CalendarItem;
use Espo\Modules\Crm\Tools\Calendar\Service;
use stdClass;
/**
* Get timeline items.
*/
class GetTimeline implements Action
{
private const MAX_CALENDAR_RANGE = 123;
public function __construct(
private Service $calendarService,
private Acl $acl
) {}
public function process(Request $request, Response $response): void
{
if (!$this->acl->check('Calendar')) {
throw new Forbidden();
}
$from = $request->getQueryParam('from');
$to = $request->getQueryParam('to');
if (empty($from) || empty($to)) {
throw new BadRequest();
}
if (strtotime($to) - strtotime($from) > self::MAX_CALENDAR_RANGE * 24 * 3600) {
throw new Forbidden('Too long range.');
}
$scopeList = null;
if ($request->getQueryParam('scopeList') !== null) {
$scopeList = explode(',', $request->getQueryParam('scopeList'));
}
$userId = $request->getQueryParam('userId');
$userIdList = $request->getQueryParam('userIdList');
$userIdList = $userIdList ? explode(',', $userIdList) : [];
if ($userId) {
$userIdList[] = $userId;
}
$fetchParams = FetchParams
::create(
DateTime::fromString($from . ':00'),
DateTime::fromString($to . ':00')
)
->withScopeList($scopeList);
$map = $this->calendarService->fetchTimelineForUsers($userIdList, $fetchParams);
$result = (object) [];
foreach ($map as $userId => $itemList) {
$result->$userId = self::itemListToRaw($itemList);
}
$response->writeBody(Json::encode($result));
}
/**
* @param CalendarItem[] $itemList
* @return stdClass[]
*/
private static function itemListToRaw(array $itemList): array
{
return array_map(fn (CalendarItem $item) => $item->getRaw(), $itemList);
}
}

View File

@@ -328,7 +328,7 @@ define('crm:views/scheduler/scheduler', ['view', 'lib!vis'], function (Dep, Vis)
let toString = to.utc().format(this.getDateTime().internalDateTimeFormat);
let url =
'Activities/action/busyRanges?from=' + fromString + '&to=' + toString +
'Timeline/busyRanges?from=' + fromString + '&to=' + toString +
'&userIdList=' + encodeURIComponent(this.userIdList.join(',')) +
'&entityType=' + this.model.entityType;

View File

@@ -101,83 +101,83 @@ class RouteTest extends \PHPUnit\Framework\TestCase
['Crm']
);
$expected = array (
array (
$expected = [
[
'adjustedRoute' => '/Custom/{scope}/{id}/{name}',
'route' => '/Custom/:scope/:id/:name',
'method' => 'get',
'params' =>
array (
[
'controller' => 'Custom',
'action' => 'list',
'scope' => ':scope',
'id' => ':id',
'name' => ':name',
),
),
array (
],
],
[
'adjustedRoute' => '/Activities/{scope}/{id}/{name}',
'route' => '/Activities/:scope/:id/:name',
'method' => 'get',
'params' =>
array (
[
'controller' => 'Activities',
'action' => 'list',
'scope' => ':scope',
'id' => ':id',
'name' => ':name',
),
),
array (
],
],
[
'adjustedRoute' => '/Activities',
'route' => '/Activities',
'method' => 'get',
'params' =>
array (
[
'controller' => 'Activities',
'action' => 'listCalendarEvents',
),
),
array (
],
],
[
'adjustedRoute' => '/App/user',
'route' => '/App/user',
'method' => 'get',
'params' =>
array (
[
'controller' => 'App',
'action' => 'user',
),
),
array (
],
],
[
'adjustedRoute' => '/Metadata',
'route' => '/Metadata',
'method' => 'get',
'params' =>
array (
[
'controller' => 'Metadata',
),
),
array (
],
],
[
'adjustedRoute' => '/{controller}/action/{action}',
'route' => '/:controller/action/:action',
'method' => 'post',
'params' =>
array (
[
'controller' => ':controller',
'action' => ':action',
),
),
array (
],
],
[
'adjustedRoute' => '/{controller}/action/{action}',
'route' => '/:controller/action/:action',
'method' => 'get',
'params' =>
array (
[
'controller' => ':controller',
'action' => ':action',
),
),
);
],
],
];
$expectedItemList = array_map(
function (array $item) {
@@ -186,7 +186,8 @@ class RouteTest extends \PHPUnit\Framework\TestCase
$item['route'],
$item['adjustedRoute'],
$item['params'] ?? [],
$item['noAuth'] ?? false
$item['noAuth'] ?? false,
null
);
},
$expected
@@ -206,80 +207,80 @@ class RouteTest extends \PHPUnit\Framework\TestCase
['Crm', 'Test']
);
$expected = array (
array (
$expected = [
[
'adjustedRoute' => '/Activities/{scope}/{id}/{name}',
'route' => '/Activities/:scope/:id/:name',
'method' => 'get',
'params' =>
array (
[
'controller' => 'Activities',
'action' => 'list',
'scope' => ':scope',
'id' => ':id',
'name' => ':name',
),
),
array (
],
],
[
'adjustedRoute' => '/Activities',
'route' => '/Activities',
'method' => 'get',
'params' =>
array (
[
'controller' => 'Activities',
'action' => 'listCalendarEvents',
),
),
array (
],
],
[
'adjustedRoute' => '/Test',
'route' => '/Test',
'method' => 'get',
'params' =>
array (
[
'controller' => 'Test',
'action' => 'listCalendarEvents',
),
),
array (
],
],
[
'adjustedRoute' => '/App/user',
'route' => '/App/user',
'method' => 'get',
'params' =>
array (
[
'controller' => 'App',
'action' => 'user',
),
),
array (
],
],
[
'adjustedRoute' => '/Metadata',
'route' => '/Metadata',
'method' => 'get',
'params' =>
array (
[
'controller' => 'Metadata',
),
),
array (
],
],
[
'adjustedRoute' => '/{controller}/action/{action}',
'route' => '/:controller/action/:action',
'method' => 'post',
'params' =>
array (
[
'controller' => ':controller',
'action' => ':action',
),
),
array (
],
],
[
'adjustedRoute' => '/{controller}/action/{action}',
'route' => '/:controller/action/:action',
'method' => 'get',
'params' =>
array (
[
'controller' => ':controller',
'action' => ':action',
),
),
);
],
],
];
$expectedItemList = array_map(
function (array $item) {
@@ -288,7 +289,8 @@ class RouteTest extends \PHPUnit\Framework\TestCase
$item['route'],
$item['adjustedRoute'],
$item['params'] ?? [],
$item['noAuth'] ?? false
$item['noAuth'] ?? false,
null
);
},
$expected
@@ -308,80 +310,80 @@ class RouteTest extends \PHPUnit\Framework\TestCase
['Crm', 'Test']
);
$expected = array (
array (
$expected = [
[
'adjustedRoute' => '/Activities/{scope}/{id}/{name}',
'route' => '/Activities/:scope/:id/:name',
'method' => 'get',
'params' =>
array (
[
'controller' => 'Test',
'action' => 'list',
'scope' => ':scope',
'id' => ':id',
'name' => ':name',
),
),
array (
],
],
[
'adjustedRoute' => '/Activities',
'route' => '/Activities',
'method' => 'get',
'params' =>
array (
[
'controller' => 'Activities',
'action' => 'listCalendarEvents',
),
),
array (
],
],
[
'adjustedRoute' => '/Test',
'route' => '/Test',
'method' => 'get',
'params' =>
array (
[
'controller' => 'Test',
'action' => 'listCalendarEvents',
),
),
array (
],
],
[
'adjustedRoute' => '/App/user',
'route' => '/App/user',
'method' => 'get',
'params' =>
array (
[
'controller' => 'App',
'action' => 'user',
),
),
array (
],
],
[
'adjustedRoute' => '/Metadata',
'route' => '/Metadata',
'method' => 'get',
'params' =>
array (
[
'controller' => 'Metadata',
),
),
array (
],
],
[
'adjustedRoute' => '/{controller}/action/{action}',
'route' => '/:controller/action/:action',
'method' => 'post',
'params' =>
array (
[
'controller' => ':controller',
'action' => ':action',
),
),
array (
],
],
[
'adjustedRoute' => '/{controller}/action/{action}',
'route' => '/:controller/action/:action',
'method' => 'get',
'params' =>
array (
[
'controller' => ':controller',
'action' => ':action',
),
),
);
],
],
];
$expectedItemList = array_map(
function (array $item) {
@@ -390,7 +392,8 @@ class RouteTest extends \PHPUnit\Framework\TestCase
$item['route'],
$item['adjustedRoute'],
$item['params'] ?? [],
$item['noAuth'] ?? false
$item['noAuth'] ?? false,
false
);
},
$expected
@@ -459,7 +462,8 @@ class RouteTest extends \PHPUnit\Framework\TestCase
$item['route'],
$item['adjustedRoute'],
$item['params'] ?? [],
$item['noAuth'] ?? false
$item['noAuth'] ?? false,
false
);
},
$expected