From e22f8b012015bfd3cdef4a011d7719950e916bb8 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Fri, 14 Oct 2022 16:34:27 +0300 Subject: [PATCH] ref --- application/Espo/Controllers/Notification.php | 15 +- application/Espo/Entities/Note.php | 17 +- application/Espo/Entities/Notification.php | 16 +- application/Espo/Services/Notification.php | 252 -------------- .../Espo/Tools/Notification/RecordService.php | 321 ++++++++++++++++++ .../Espo/Tools/Notification/Service.php | 11 +- .../Espo/Tools/Stream/NoteAccessControl.php | 2 +- 7 files changed, 352 insertions(+), 282 deletions(-) create mode 100644 application/Espo/Tools/Notification/RecordService.php diff --git a/application/Espo/Controllers/Notification.php b/application/Espo/Controllers/Notification.php index 76fa866ac7..96823a1286 100644 --- a/application/Espo/Controllers/Notification.php +++ b/application/Espo/Controllers/Notification.php @@ -29,12 +29,15 @@ namespace Espo\Controllers; -use Espo\Services\Notification as Service; +use Espo\Tools\Notification\RecordService as Service; use Espo\Core\{ Controllers\RecordBase, Api\Request, Api\Response, + Exceptions\BadRequest, + Exceptions\Error, + Exceptions\Forbidden }; use stdClass; @@ -43,6 +46,11 @@ class Notification extends RecordBase { public static $defaultAction = 'list'; + /** + * @throws BadRequest + * @throws Forbidden + * @throws Error + */ public function getActionList(Request $request, Response $response): stdClass { $userId = $this->user->getId(); @@ -60,7 +68,7 @@ class Notification extends RecordBase 'after' => $after, ]; - $recordCollection = $this->getNotificationService()->getList($userId, $params); + $recordCollection = $this->getNotificationService()->get($userId, $params); return (object) [ 'total' => $recordCollection->getTotal(), @@ -86,7 +94,6 @@ class Notification extends RecordBase private function getNotificationService(): Service { - /** @var Service */ - return $this->recordServiceContainer->get('Notification'); + return $this->injectableFactory->create(Service::class); } } diff --git a/application/Espo/Entities/Note.php b/application/Espo/Entities/Note.php index 4353ebc114..86eaf8b6df 100644 --- a/application/Espo/Entities/Note.php +++ b/application/Espo/Entities/Note.php @@ -41,31 +41,19 @@ class Note extends Entity public const ENTITY_TYPE = 'Note'; public const TARGET_SELF = 'self'; - public const TARGET_ALL = 'all'; - public const TARGET_TEAMS = 'teams'; - public const TARGET_USERS = 'users'; - public const TARGET_PORTALS = 'portals'; public const TYPE_POST = 'Post'; - public const TYPE_UPDATE = 'Update'; - public const TYPE_STATUS = 'Status'; - public const TYPE_CREATE = 'Create'; - public const TYPE_CREATE_RELATED = 'CreateRelated'; - public const TYPE_RELATE = 'Relate'; - public const TYPE_ASSIGN = 'Assign'; - public const TYPE_EMAIL_RECEIVED = 'EmailReceived'; - public const TYPE_EMAIL_SENT = 'EmailSent'; private bool $aclIsProcessed = false; @@ -75,6 +63,11 @@ class Note extends Entity return $this->getType() === self::TYPE_POST; } + public function isGlobal(): bool + { + return (bool) $this->get('isGlobal'); + } + public function getType(): ?string { return $this->get('type'); diff --git a/application/Espo/Entities/Notification.php b/application/Espo/Entities/Notification.php index c2d21a943f..b7985d5b4d 100644 --- a/application/Espo/Entities/Notification.php +++ b/application/Espo/Entities/Notification.php @@ -38,19 +38,18 @@ class Notification extends \Espo\Core\ORM\Entity public const ENTITY_TYPE = 'Notification'; public const TYPE_ENTITY_REMOVED = 'EntityRemoved'; - public const TYPE_ASSIGN = 'Assign'; - public const TYPE_EMAIL_RECEIVED = 'EmailReceived'; - public const TYPE_NOTE = 'Note'; - public const TYPE_MENTION_IN_POST = 'MentionInPost'; - public const TYPE_MESSAGE = 'Message'; - public const TYPE_SYSTEM = 'System'; + public function getType(): ?string + { + return $this->get('type'); + } + public function setMessage(?string $message): self { $this->set('message', $message); @@ -65,6 +64,11 @@ class Notification extends \Espo\Core\ORM\Entity return $this; } + public function getData(): ?stdClass + { + return $this->get('data'); + } + public function setData(stdClass $data): self { $this->set('data', $data); diff --git a/application/Espo/Services/Notification.php b/application/Espo/Services/Notification.php index fc62806167..13b895c535 100644 --- a/application/Espo/Services/Notification.php +++ b/application/Espo/Services/Notification.php @@ -29,262 +29,10 @@ namespace Espo\Services; -use Espo\Core\{ - Record\Collection as RecordCollection, - Exceptions\Error, -}; - -use Espo\Entities\User; - -use Espo\Tools\Stream\NoteAccessControl; - -use ArrayAccess; - /** * @extends Record<\Espo\Entities\Notification> */ class Notification extends \Espo\Services\Record { protected $actionHistoryDisabled = true; - - private ?NoteAccessControl $noteAccessControl = null; - - public function getNotReadCount(string $userId): int - { - $whereClause = [ - 'userId' => $userId, - 'read' => false, - ]; - - $ignoreScopeList = $this->getIgnoreScopeList(); - - if (count($ignoreScopeList)) { - $whereClause[] = [ - 'OR' => [ - 'relatedParentType' => null, - 'relatedParentType!=' => $ignoreScopeList, - ] - ]; - } - - return $this->entityManager - ->getRDBRepository('Notification') - ->where($whereClause) - ->count(); - } - - public function markAllRead(string $userId): bool - { - $update = $this->entityManager - ->getQueryBuilder() - ->update() - ->in('Notification') - ->set(['read' => true]) - ->where([ - 'userId' => $userId, - 'read' => false, - ]) - ->build(); - - $this->entityManager->getQueryExecutor()->execute($update); - - return true; - } - - /** - * @param array{ - * after?: ?string, - * offset?: ?int, - * maxSize?: ?int, - * } $params - * @return RecordCollection<\Espo\Entities\Notification> - * @throws Error - */ - public function getList(string $userId, array $params = []): RecordCollection - { - $queryBuilder = $this->entityManager - ->getQueryBuilder() - ->select() - ->from('Notification'); - - $whereClause = [ - 'userId' => $userId, - ]; - - $user = $this->entityManager->getEntity(User::ENTITY_TYPE, $userId); - - if (!$user) { - throw new Error("User not found."); - } - - if (!empty($params['after'])) { - $whereClause['createdAt>'] = $params['after']; - } - - $ignoreScopeList = $this->getIgnoreScopeList(); - - if (!empty($ignoreScopeList)) { - $where = []; - - $where[] = [ - 'OR' => [ - 'relatedParentType' => null, - 'relatedParentType!=' => $ignoreScopeList - ] - ]; - - $whereClause[] = $where; - } - - $offset = $params['offset'] ?? null; - $maxSize = $params['maxSize'] ?? null; - - $queryBuilder->limit($offset, $maxSize); - - $queryBuilder->order('createdAt', 'DESC'); - - $queryBuilder->where($whereClause); - - $query = $queryBuilder->build(); - - /** @var \Espo\ORM\Collection<\Espo\Entities\Notification> $collection > */ - $collection = $this->entityManager - ->getRDBRepository('Notification') - ->clone($query) - ->find(); - - if (!$collection instanceof ArrayAccess) { - throw new Error("Collection is not instance of ArrayAccess."); - } - - $count = $this->entityManager - ->getRDBRepository('Notification') - ->clone($query) - ->count(); - - $ids = []; - - foreach ($collection as $k => $entity) { - $ids[] = $entity->getId(); - - $data = $entity->get('data'); - - if (empty($data)) { - continue; - } - - switch ($entity->get('type')) { - case 'Note': - case 'MentionInPost': - /** @var ?\Espo\Entities\Note $note */ - $note = $this->entityManager->getEntity('Note', $data->noteId); - - if (!$note) { - unset($collection[$k]); - - $count--; - - $this->entityManager->removeEntity($entity); - - break; - } - - $this->getNoteAccessControl()->apply($note, $user); - - if ($note->get('parentId') && $note->get('parentType')) { - $parent = $this->entityManager - ->getEntity($note->get('parentType'), $note->get('parentId')); - - if ($parent) { - $note->set('parentName', $parent->get('name')); - } - } - else { - if (!$note->get('isGlobal')) { - $targetType = $note->get('targetType'); - - if (!$targetType || $targetType === 'users') { - $note->loadLinkMultipleField('users'); - } - - if ($targetType !== 'users') { - if (!$targetType || $targetType === 'teams') { - $note->loadLinkMultipleField('teams'); - } - else if ($targetType === 'portals') { - $note->loadLinkMultipleField('portals'); - } - } - } - } - - if ($note->get('relatedId') && $note->get('relatedType')) { - $related = $this->entityManager - ->getEntity($note->get('relatedType'), $note->get('relatedId')); - - if ($related) { - $note->set('relatedName', $related->get('name')); - } - } - - $note->loadLinkMultipleField('attachments'); - - $entity->set('noteData', $note->toArray()); - - break; - } - } - - if (!empty($ids)) { - $update = $this->entityManager - ->getQueryBuilder() - ->update() - ->in('Notification') - ->set(['read' => true]) - ->where([ - 'id' => $ids, - ]) - ->build(); - - $this->entityManager->getQueryExecutor()->execute($update); - } - - /** @return RecordCollection<\Espo\Entities\Notification> */ - return new RecordCollection($collection, $count); - } - - /** - * @return string[] - */ - private function getIgnoreScopeList(): array - { - $ignoreScopeList = []; - - $scopes = $this->metadata->get('scopes', []); - - foreach ($scopes as $scope => $d) { - if (empty($d['entity'])) { - continue; - } - - if (empty($d['object'])) { - continue; - } - - if (!$this->acl->checkScope($scope)) { - $ignoreScopeList[] = $scope; - } - } - - return $ignoreScopeList; - } - - private function getNoteAccessControl(): NoteAccessControl - { - if (!$this->noteAccessControl) { - $this->noteAccessControl = $this->injectableFactory->create(NoteAccessControl::class); - } - - return $this->noteAccessControl; - } } diff --git a/application/Espo/Tools/Notification/RecordService.php b/application/Espo/Tools/Notification/RecordService.php new file mode 100644 index 0000000000..be65e676af --- /dev/null +++ b/application/Espo/Tools/Notification/RecordService.php @@ -0,0 +1,321 @@ +entityManager = $entityManager; + $this->acl = $acl; + $this->metadata = $metadata; + $this->noteAccessControl = $noteAccessControl; + } + + /** + * @todo Use params class FetchParams. + * + * @param array{ + * after?: ?string, + * offset?: ?int, + * maxSize?: ?int, + * } $params + * @return RecordCollection + * @throws Error + */ + public function get(string $userId, array $params = []): RecordCollection + { + $queryBuilder = $this->entityManager + ->getQueryBuilder() + ->select() + ->from(Notification::ENTITY_TYPE); + + $whereClause = ['userId' => $userId]; + + $user = $this->entityManager + ->getRDBRepositoryByClass(User::class) + ->getById($userId); + + if (!$user) { + throw new Error("User not found."); + } + + if (!empty($params['after'])) { + $whereClause['createdAt>'] = $params['after']; + } + + $ignoreScopeList = $this->getIgnoreScopeList(); + + if (!empty($ignoreScopeList)) { + $where = []; + + $where[] = [ + 'OR' => [ + 'relatedParentType' => null, + 'relatedParentType!=' => $ignoreScopeList, + ] + ]; + + $whereClause[] = $where; + } + + $offset = $params['offset'] ?? null; + $maxSize = $params['maxSize'] ?? null; + + $queryBuilder + ->limit($offset, $maxSize) + ->order('createdAt', 'DESC') + ->where($whereClause); + + $query = $queryBuilder->build(); + + $collection = $this->entityManager + ->getRDBRepositoryByClass(Notification::class) + ->clone($query) + ->find(); + + if (!$collection instanceof EntityCollection) { + throw new Error("Collection is not instance of EntityCollection."); + } + + $count = $this->entityManager + ->getRDBRepositoryByClass(Notification::class) + ->clone($query) + ->count(); + + $ids = []; + + foreach ($collection as $k => $entity) { + $ids[] = $entity->getId(); + + $this->prepareListItem($entity, $k, $collection, $count, $user); + } + + $this->markAsRead($ids); + + return RecordCollection::create($collection, $count); + } + + /** + * @param string[] $ids + */ + private function markAsRead(array $ids): void + { + if ($ids === []) { + return; + } + + $query = $this->entityManager + ->getQueryBuilder() + ->update() + ->in(Notification::ENTITY_TYPE) + ->set(['read' => true]) + ->where(['id' => $ids]) + ->build(); + + $this->entityManager->getQueryExecutor()->execute($query); + } + + /** + * @param EntityCollection $collection + */ + private function prepareListItem( + Notification $entity, + int $index, + EntityCollection $collection, + int &$count, + User $user + ): void { + + $data = $entity->getData(); + + if ($data === null) { + return; + } + + $noteId = $data->noteId ?? null; + + if (!$noteId) { + return; + } + + $type = $entity->getType(); + + if (!in_array($type, [Notification::TYPE_NOTE, Notification::TYPE_MENTION_IN_POST])) { + return; + } + + /** @var ?Note $note */ + $note = $this->entityManager + ->getRDBRepositoryByClass(Note::class) + ->getById($noteId); + + if (!$note) { + unset($collection[$index]); + $count--; + + $this->entityManager->removeEntity($entity); + + return; + } + + $this->noteAccessControl->apply($note, $user); + + $parentId = $note->getParentId(); + $parentType = $note->getParentType(); + + if ($parentId && $parentType) { + $parent = $this->entityManager->getEntityById($parentType, $parentId); + + if ($parent) { + $note->set('parentName', $parent->get('name')); + } + } + else if (!$note->isGlobal()) { + $targetType = $note->getTargetType(); + + if (!$targetType || $targetType === Note::TARGET_USERS) { + $note->loadLinkMultipleField('users'); + } + + if ($targetType !== Note::TARGET_USERS) { + if (!$targetType || $targetType === Note::TARGET_TEAMS) { + $note->loadLinkMultipleField('teams'); + } + else if ($targetType === Note::TARGET_PORTALS) { + $note->loadLinkMultipleField('portals'); + } + } + } + + $relatedId = $note->getRelatedId(); + $relatedType = $note->getRelatedType(); + + if ($relatedId && $relatedType) { + $related = $this->entityManager->getEntityById($relatedType, $relatedId); + + if ($related) { + $note->set('relatedName', $related->get('name')); + } + } + + $note->loadLinkMultipleField('attachments'); + + $entity->set('noteData', $note->getValueMap()); + } + + public function getNotReadCount(string $userId): int + { + $whereClause = [ + 'userId' => $userId, + 'read' => false, + ]; + + $ignoreScopeList = $this->getIgnoreScopeList(); + + if (count($ignoreScopeList)) { + $whereClause[] = [ + 'OR' => [ + 'relatedParentType' => null, + 'relatedParentType!=' => $ignoreScopeList, + ] + ]; + } + + return $this->entityManager + ->getRDBRepositoryByClass(Note::class) + ->where($whereClause) + ->count(); + } + + public function markAllRead(string $userId): bool + { + $update = $this->entityManager + ->getQueryBuilder() + ->update() + ->in(Notification::ENTITY_TYPE) + ->set(['read' => true]) + ->where([ + 'userId' => $userId, + 'read' => false, + ]) + ->build(); + + $this->entityManager->getQueryExecutor()->execute($update); + + return true; + } + + /** + * @return string[] + */ + private function getIgnoreScopeList(): array + { + $ignoreScopeList = []; + + $scopes = $this->metadata->get('scopes', []); + + foreach ($scopes as $scope => $item) { + if (empty($item['entity'])) { + continue; + } + + if (empty($item['object'])) { + continue; + } + + if (!$this->acl->checkScope($scope)) { + $ignoreScopeList[] = $scope; + } + } + + return $ignoreScopeList; + } +} diff --git a/application/Espo/Tools/Notification/Service.php b/application/Espo/Tools/Notification/Service.php index 7f22193f66..4a5781cb32 100644 --- a/application/Espo/Tools/Notification/Service.php +++ b/application/Espo/Tools/Notification/Service.php @@ -44,13 +44,10 @@ use Espo\ORM\EntityManager; class Service { - private $entityManager; - - private $config; - - private $aclManager; - - private $webSocketSubmission; + private EntityManager $entityManager; + private Config $config; + private AclManager $aclManager; + private Submission $webSocketSubmission; public function __construct( EntityManager $entityManager, diff --git a/application/Espo/Tools/Stream/NoteAccessControl.php b/application/Espo/Tools/Stream/NoteAccessControl.php index b3d87f2bec..09c1e51231 100644 --- a/application/Espo/Tools/Stream/NoteAccessControl.php +++ b/application/Espo/Tools/Stream/NoteAccessControl.php @@ -36,7 +36,7 @@ use Espo\Core\Utils\Acl\UserAclManagerProvider; class NoteAccessControl { - private $userAclManagerProvider; + private UserAclManagerProvider $userAclManagerProvider; public function __construct(UserAclManagerProvider $userAclManagerProvider) {