From f46c2d607955b5da8f7e697271976fbfc2511bab Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Fri, 23 Feb 2024 10:23:26 +0200 Subject: [PATCH] stream followers find refactoring --- application/Espo/Core/Record/Service.php | 32 ++------ application/Espo/Resources/routes.json | 5 ++ .../Espo/Tools/Stream/Api/DeleteFollowers.php | 9 ++- .../Espo/Tools/Stream/Api/GetFollowers.php | 75 +++++++++++++++++++ .../Espo/Tools/Stream/Api/PostFollowers.php | 9 ++- .../Tools/Stream/FollowerRecordService.php | 66 ++++++++++++++-- 6 files changed, 163 insertions(+), 33 deletions(-) create mode 100644 application/Espo/Tools/Stream/Api/GetFollowers.php diff --git a/application/Espo/Core/Record/Service.php b/application/Espo/Core/Record/Service.php index 92f15ca639..c0db9bd285 100644 --- a/application/Espo/Core/Record/Service.php +++ b/application/Espo/Core/Record/Service.php @@ -1180,16 +1180,19 @@ class Service implements Crud, * @throws Forbidden * @throws NotFound * @throws BadRequest + * @todo Remove in v9.0. */ private function processFindLinkedMethod(string $id, string $link, SearchParams $searchParams): ?RecordCollection { - if ($link === 'followers') { - return $this->findLinkedFollowers($id, $searchParams); - } - $methodName = 'findLinked' . ucfirst($link); if (method_exists($this, $methodName)) { + trigger_error( + "Usage of $methodName method is deprecated and will be removed in v9.0.", + E_USER_DEPRECATED + ); + + return $this->$methodName($id, $searchParams); } @@ -1697,27 +1700,6 @@ class Service implements Crud, } } - /** - * @return RecordCollection - * @throws NotFound - * @throws Forbidden - * @throws BadRequest - */ - protected function findLinkedFollowers(string $id, SearchParams $params): RecordCollection - { - $entity = $this->getRepository()->getById($id); - - if (!$entity) { - throw new NotFound(); - } - - if (!$this->acl->check($entity, AclTable::ACTION_READ)) { - throw new Forbidden(); - } - - return $this->getStreamService()->findEntityFollowers($entity, $params); - } - private function createEntityDuplicator(): EntityDuplicator { return $this->injectableFactory->create(EntityDuplicator::class); diff --git a/application/Espo/Resources/routes.json b/application/Espo/Resources/routes.json index 71a3c9d484..8fc87b425e 100644 --- a/application/Espo/Resources/routes.json +++ b/application/Espo/Resources/routes.json @@ -501,6 +501,11 @@ "id": ":id" } }, + { + "route": "/:entityType/:id/followers", + "method": "get", + "actionClassName": "Espo\\Tools\\Stream\\Api\\GetFollowers" + }, { "route": "/:entityType/:id/followers", "method": "post", diff --git a/application/Espo/Tools/Stream/Api/DeleteFollowers.php b/application/Espo/Tools/Stream/Api/DeleteFollowers.php index f791dd4d50..dab88d3cf1 100644 --- a/application/Espo/Tools/Stream/Api/DeleteFollowers.php +++ b/application/Espo/Tools/Stream/Api/DeleteFollowers.php @@ -29,11 +29,13 @@ namespace Espo\Tools\Stream\Api; +use Espo\Core\Acl; use Espo\Core\Api\Action; use Espo\Core\Api\Request; use Espo\Core\Api\Response; use Espo\Core\Api\ResponseComposer; use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Exceptions\Forbidden; use Espo\Tools\Stream\FollowerRecordService; /** @@ -42,7 +44,8 @@ use Espo\Tools\Stream\FollowerRecordService; class DeleteFollowers implements Action { public function __construct( - private FollowerRecordService $service + private FollowerRecordService $service, + private Acl $acl ) {} public function process(Request $request): Response @@ -56,6 +59,10 @@ class DeleteFollowers implements Action throw new BadRequest(); } + if (!$this->acl->check($entityType)) { + throw new Forbidden(); + } + $ids = $data->ids ?? (isset($data->id) ? [$data->id] : []); if ($ids === [] || !is_array($ids)) { diff --git a/application/Espo/Tools/Stream/Api/GetFollowers.php b/application/Espo/Tools/Stream/Api/GetFollowers.php new file mode 100644 index 0000000000..8b35e5a7db --- /dev/null +++ b/application/Espo/Tools/Stream/Api/GetFollowers.php @@ -0,0 +1,75 @@ +. + * + * 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\Tools\Stream\Api; + +use Espo\Core\Acl; +use Espo\Core\Api\Action; +use Espo\Core\Api\Request; +use Espo\Core\Api\Response; +use Espo\Core\Api\ResponseComposer; +use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Exceptions\Forbidden; +use Espo\Core\Record\SearchParamsFetcher; +use Espo\Tools\Stream\FollowerRecordService; + +/** + * @noinspection PhpUnused + */ +class GetFollowers implements Action +{ + public function __construct( + private FollowerRecordService $service, + private SearchParamsFetcher $searchParamsFetcher, + private Acl $acl + ) {} + + public function process(Request $request): Response + { + $entityType = $request->getRouteParam('entityType'); + $id = $request->getRouteParam('id'); + + if (!$entityType || !$id) { + throw new BadRequest(); + } + + if (!$this->acl->check($entityType)) { + throw new Forbidden(); + } + + $searchParams = $this->searchParamsFetcher->fetch($request); + + $collection = $this->service->find($entityType, $id, $searchParams); + + return ResponseComposer::json((object) [ + 'total' => $collection->getTotal(), + 'list' => $collection->getValueMapList(), + ]); + } +} diff --git a/application/Espo/Tools/Stream/Api/PostFollowers.php b/application/Espo/Tools/Stream/Api/PostFollowers.php index 7f9b3dfb37..dc62454f51 100644 --- a/application/Espo/Tools/Stream/Api/PostFollowers.php +++ b/application/Espo/Tools/Stream/Api/PostFollowers.php @@ -29,11 +29,13 @@ namespace Espo\Tools\Stream\Api; +use Espo\Core\Acl; use Espo\Core\Api\Action; use Espo\Core\Api\Request; use Espo\Core\Api\Response; use Espo\Core\Api\ResponseComposer; use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Exceptions\Forbidden; use Espo\Tools\Stream\FollowerRecordService; /** @@ -42,7 +44,8 @@ use Espo\Tools\Stream\FollowerRecordService; class PostFollowers implements Action { public function __construct( - private FollowerRecordService $service + private FollowerRecordService $service, + private Acl $acl ) {} public function process(Request $request): Response @@ -56,6 +59,10 @@ class PostFollowers implements Action throw new BadRequest(); } + if (!$this->acl->check($entityType)) { + throw new Forbidden(); + } + $ids = $data->ids ?? (isset($data->id) ? [$data->id] : []); if ($ids === [] || !is_array($ids)) { diff --git a/application/Espo/Tools/Stream/FollowerRecordService.php b/application/Espo/Tools/Stream/FollowerRecordService.php index 639cf8781d..8051ac0ed6 100644 --- a/application/Espo/Tools/Stream/FollowerRecordService.php +++ b/application/Espo/Tools/Stream/FollowerRecordService.php @@ -29,9 +29,12 @@ namespace Espo\Tools\Stream; +use Espo\Core\Exceptions\BadRequest; use Espo\Core\Exceptions\Error\Body as ErrorBody; use Espo\Core\Exceptions\Forbidden; use Espo\Core\Exceptions\NotFound; +use Espo\Core\Record\Collection; +use Espo\Core\Select\SearchParams; use Espo\Core\Utils\Metadata; use Espo\ORM\Entity; use Espo\ORM\EntityManager; @@ -48,6 +51,23 @@ class FollowerRecordService private Service $service ) {} + /** + * Find followers. + * + * @return Collection + * @throws Forbidden + * @throws NotFound + * @throws BadRequest + */ + public function find(string $entityType, string $id, SearchParams $params): Collection + { + $this->checkReadAccess($entityType); + + $entity = $this->getEntity($entityType, $id); + + return $this->service->findEntityFollowers($entity, $params); + } + /** * Add a user to followers. * @@ -56,9 +76,9 @@ class FollowerRecordService */ public function link(string $entityType, string $id, string $userId): void { - $this->checkAccess($entityType); + $this->checkEditAccess($entityType); - $entity = $this->getEntity($entityType, $id); + $entity = $this->getEntityForEdit($entityType, $id); $user = $this->getUser($userId); $this->follow($entity, $user); @@ -72,9 +92,9 @@ class FollowerRecordService */ public function unlink(string $entityType, string $id, string $userId): void { - $this->checkAccess($entityType); + $this->checkEditAccess($entityType); - $entity = $this->getEntity($entityType, $id); + $entity = $this->getEntityForEdit($entityType, $id); $user = $this->getUser($userId); $this->service->unfollowEntity($entity, $user->getId()); @@ -89,10 +109,25 @@ class FollowerRecordService * @throws Forbidden * @throws NotFound */ - private function checkAccess(string $entityType): void + private function checkReadAccess(string $entityType): void + { + if (!$this->acl->check($entityType, Acl\Table::ACTION_READ)) { + throw new Forbidden("No 'read'' access to $entityType scope."); + } + + if (!$this->hasStream($entityType)) { + throw new NotFound("No stream."); + } + } + + /** + * @throws Forbidden + * @throws NotFound + */ + private function checkEditAccess(string $entityType): void { if (!$this->acl->check($entityType, Acl\Table::ACTION_EDIT)) { - throw new Forbidden("No edit access to $entityType scope."); + throw new Forbidden("No 'edit' access to $entityType scope."); } if (!$this->hasStream($entityType)) { @@ -133,6 +168,25 @@ class FollowerRecordService throw new NotFound("Record not found."); } + if (!$this->acl->check($entity, Acl\Table::ACTION_READ)) { + throw new Forbidden("No 'read' access."); + } + + return $entity; + } + + /** + * @throws NotFound + * @throws Forbidden + */ + private function getEntityForEdit(string $entityType, string $id): Entity + { + $entity = $this->entityManager->getEntityById($entityType, $id); + + if (!$entity) { + throw new NotFound("Record not found."); + } + if (!$this->acl->check($entity, Acl\Table::ACTION_EDIT)) { throw new Forbidden("No 'edit' access."); }