From ace2dc802a1346979be84c67ccf162de1cb877a8 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Fri, 17 Feb 2023 20:17:18 +0200 Subject: [PATCH] api action --- application/Espo/Core/Api/Action.php | 53 +++++ application/Espo/Core/Api/ActionHandler.php | 2 +- .../Espo/Core/Api/RequestProcessor.php | 46 +++- application/Espo/Core/Api/RequestWrapper.php | 53 ++--- application/Espo/Core/Api/Response.php | 1 + application/Espo/Core/Api/ResponseWrapper.php | 28 +-- application/Espo/Core/Api/Route.php | 12 +- .../Core/ApplicationRunners/PortalClient.php | 2 +- application/Espo/Core/EntryPoint/Starter.php | 4 +- application/Espo/Core/Portal/Utils/Route.php | 3 +- application/Espo/Core/Utils/ClientManager.php | 2 +- application/Espo/Core/Utils/Route.php | 10 +- .../Modules/Crm/Controllers/Activities.php | 205 +----------------- .../Espo/Modules/Crm/Resources/routes.json | 15 +- .../Crm/Tools/Calendar/Api/GetBusyRanges.php | 87 ++++++++ .../Crm/Tools/Calendar/Api/GetCalendar.php | 142 ++++++++++++ .../Crm/Tools/Calendar/Api/GetTimeline.php | 116 ++++++++++ .../crm/src/views/scheduler/scheduler.js | 2 +- tests/unit/Espo/Core/Utils/RouteTest.php | 192 ++++++++-------- 19 files changed, 612 insertions(+), 363 deletions(-) create mode 100644 application/Espo/Core/Api/Action.php create mode 100644 application/Espo/Modules/Crm/Tools/Calendar/Api/GetBusyRanges.php create mode 100644 application/Espo/Modules/Crm/Tools/Calendar/Api/GetCalendar.php create mode 100644 application/Espo/Modules/Crm/Tools/Calendar/Api/GetTimeline.php diff --git a/application/Espo/Core/Api/Action.php b/application/Espo/Core/Api/Action.php new file mode 100644 index 0000000000..9e726e8541 --- /dev/null +++ b/application/Espo/Core/Api/Action.php @@ -0,0 +1,53 @@ +afterProceed($responseWrapped); - return $responseWrapped->getResponse(); + return $responseWrapped->toPsr7(); } private function beforeProceed(): void diff --git a/application/Espo/Core/Api/RequestProcessor.php b/application/Espo/Core/Api/RequestProcessor.php index 94f815a172..9e2617e583 100644 --- a/application/Espo/Core/Api/RequestProcessor.php +++ b/application/Espo/Core/Api/RequestProcessor.php @@ -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 $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; diff --git a/application/Espo/Core/Api/RequestWrapper.php b/application/Espo/Core/Api/RequestWrapper.php index 799e81e4de..edec984a4a 100644 --- a/application/Espo/Core/Api/RequestWrapper.php +++ b/application/Espo/Core/Api/RequestWrapper.php @@ -43,21 +43,16 @@ use stdClass; */ class RequestWrapper implements ApiRequest { - private Psr7Request $request; - private string $basePath; private ?stdClass $parsedBody = null; - /** @var array */ - private array $routeParams; /** * @param array $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 + * @return array */ 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)); } diff --git a/application/Espo/Core/Api/Response.php b/application/Espo/Core/Api/Response.php index a0c79ae4a9..e35b49c36c 100644 --- a/application/Espo/Core/Api/Response.php +++ b/application/Espo/Core/Api/Response.php @@ -29,6 +29,7 @@ namespace Espo\Core\Api; +use Psr\Http\Message\ResponseInterface as Psr7Response; use Psr\Http\Message\StreamInterface; /** diff --git a/application/Espo/Core/Api/ResponseWrapper.php b/application/Espo/Core/Api/ResponseWrapper.php index e9f0d63b6c..52159a5a57 100644 --- a/application/Espo/Core/Api/ResponseWrapper.php +++ b/application/Espo/Core/Api/ResponseWrapper.php @@ -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; } } diff --git a/application/Espo/Core/Api/Route.php b/application/Espo/Core/Api/Route.php index a961d27b72..969a0f8aeb 100644 --- a/application/Espo/Core/Api/Route.php +++ b/application/Espo/Core/Api/Route.php @@ -35,17 +35,27 @@ class Route /** * @param array $params + * @param ?class-string $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 + */ + public function getActionClassName(): ?string + { + return $this->actionClassName; + } + public function getMethod(): string { return $this->method; diff --git a/application/Espo/Core/ApplicationRunners/PortalClient.php b/application/Espo/Core/ApplicationRunners/PortalClient.php index b28819ed2d..68fee37c09 100644 --- a/application/Espo/Core/ApplicationRunners/PortalClient.php +++ b/application/Espo/Core/ApplicationRunners/PortalClient.php @@ -100,6 +100,6 @@ class PortalClient implements RunnerParameterized { $this->errorOutput->processWithBodyPrinting($request, $response, $exception); - (new ResponseEmitter())->emit($response->getResponse()); + (new ResponseEmitter())->emit($response->toPsr7()); } } diff --git a/application/Espo/Core/EntryPoint/Starter.php b/application/Espo/Core/EntryPoint/Starter.php index d1d7ad5fd3..40ba3331fd 100644 --- a/application/Espo/Core/EntryPoint/Starter.php +++ b/application/Espo/Core/EntryPoint/Starter.php @@ -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( diff --git a/application/Espo/Core/Portal/Utils/Route.php b/application/Espo/Core/Portal/Utils/Route.php index 15b6e693e2..54db6a5d66 100644 --- a/application/Espo/Core/Portal/Utils/Route.php +++ b/application/Espo/Core/Portal/Utils/Route.php @@ -54,7 +54,8 @@ class Route extends BaseRoute $route->getRoute(), $path, $route->getParams(), - $route->noAuth() + $route->noAuth(), + $route->getActionClassName() ); $newRouteList[] = $newRoute; diff --git a/application/Espo/Core/Utils/ClientManager.php b/application/Espo/Core/Utils/ClientManager.php index 27a9f83155..1bbc306e98 100644 --- a/application/Espo/Core/Utils/ClientManager.php +++ b/application/Espo/Core/Utils/ClientManager.php @@ -149,7 +149,7 @@ class ClientManager $this->writeHeaders($response); $response->writeBody($body); - (new ResponseEmitter())->emit($response->getResponse()); + (new ResponseEmitter())->emit($response->toPsr7()); } /** diff --git a/application/Espo/Core/Utils/Route.php b/application/Espo/Core/Utils/Route.php index 146143c6f6..a6175bbf6f 100644 --- a/application/Espo/Core/Utils/Route.php +++ b/application/Espo/Core/Utils/Route.php @@ -40,11 +40,12 @@ use Espo\Core\Utils\Resource\PathProvider; * method: string, * noAuth?: bool, * params?: array, + * 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 $newRoute - * @param array> $routeList + * @param RouteArrayShape $newRoute + * @param array $routeList */ static private function isRouteInList(array $newRoute, array $routeList): bool { diff --git a/application/Espo/Modules/Crm/Controllers/Activities.php b/application/Espo/Modules/Crm/Controllers/Activities.php index 9b02fda37b..bc5a874aea 100644 --- a/application/Espo/Modules/Crm/Controllers/Activities.php +++ b/application/Espo/Modules/Crm/Controllers/Activities.php @@ -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 - * @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; - } } diff --git a/application/Espo/Modules/Crm/Resources/routes.json b/application/Espo/Modules/Crm/Resources/routes.json index da60b5bff7..f9a4747aa2 100644 --- a/application/Espo/Modules/Crm/Resources/routes.json +++ b/application/Espo/Modules/Crm/Resources/routes.json @@ -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", diff --git a/application/Espo/Modules/Crm/Tools/Calendar/Api/GetBusyRanges.php b/application/Espo/Modules/Crm/Tools/Calendar/Api/GetBusyRanges.php new file mode 100644 index 0000000000..254cd72bff --- /dev/null +++ b/application/Espo/Modules/Crm/Tools/Calendar/Api/GetBusyRanges.php @@ -0,0 +1,87 @@ +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); + } +} diff --git a/application/Espo/Modules/Crm/Tools/Calendar/Api/GetCalendar.php b/application/Espo/Modules/Crm/Tools/Calendar/Api/GetCalendar.php new file mode 100644 index 0000000000..d62cf8342d --- /dev/null +++ b/application/Espo/Modules/Crm/Tools/Calendar/Api/GetCalendar.php @@ -0,0 +1,142 @@ +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); + } +} diff --git a/application/Espo/Modules/Crm/Tools/Calendar/Api/GetTimeline.php b/application/Espo/Modules/Crm/Tools/Calendar/Api/GetTimeline.php new file mode 100644 index 0000000000..390eeeb3d6 --- /dev/null +++ b/application/Espo/Modules/Crm/Tools/Calendar/Api/GetTimeline.php @@ -0,0 +1,116 @@ +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); + } +} diff --git a/client/modules/crm/src/views/scheduler/scheduler.js b/client/modules/crm/src/views/scheduler/scheduler.js index 1b4d9da24d..b848007a3c 100644 --- a/client/modules/crm/src/views/scheduler/scheduler.js +++ b/client/modules/crm/src/views/scheduler/scheduler.js @@ -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; diff --git a/tests/unit/Espo/Core/Utils/RouteTest.php b/tests/unit/Espo/Core/Utils/RouteTest.php index 17bdb02c72..18e0a4fd5a 100644 --- a/tests/unit/Espo/Core/Utils/RouteTest.php +++ b/tests/unit/Espo/Core/Utils/RouteTest.php @@ -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