diff --git a/application/Espo/Acl/Email.php b/application/Espo/Acl/Email.php index 9b67d1e7a9..8e16fa9530 100644 --- a/application/Espo/Acl/Email.php +++ b/application/Espo/Acl/Email.php @@ -42,8 +42,6 @@ use Espo\Core\{ class Email extends Acl implements EntityReadAcl { - protected $ownerUserIdAttribute = 'usersIds'; - public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($this->checkEntity($user, $entity, $data, 'read')) { diff --git a/application/Espo/AclPortal/Email.php b/application/Espo/AclPortal/Email.php index b5a16c6ce2..eb3d005247 100644 --- a/application/Espo/AclPortal/Email.php +++ b/application/Espo/AclPortal/Email.php @@ -42,8 +42,6 @@ use Espo\Core\{ class Email extends Acl implements EntityReadAcl { - protected $ownerUserIdAttribute = 'usersIds'; - public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($this->checkEntity($user, $entity, $data, Table::ACTION_READ)) { diff --git a/application/Espo/Core/Acl/Acl.php b/application/Espo/Core/Acl/Acl.php index 319ac98b57..0e8f2c16ab 100644 --- a/application/Espo/Core/Acl/Acl.php +++ b/application/Espo/Core/Acl/Acl.php @@ -288,38 +288,6 @@ class Acl implements ScopeAcl, EntityAcl, EntityDeleteAcl return true; } - public function getOwnerUserIdAttribute(): ?string - { - if ($this->ownerUserIdAttribute) { - return $this->ownerUserIdAttribute; - } - - if (!$this->scope) { - return null; - } - - $defs = $this->entityManager->getDefs()->getEntity($this->scope); - - if ( - $defs->hasField(self::FIELD_ASSIGNED_USERS) && - $defs->getField(self::FIELD_ASSIGNED_USERS)->getType() === 'linkMultiple' && - $defs->hasRelation(self::FIELD_ASSIGNED_USERS) && - $defs->getRelation(self::FIELD_ASSIGNED_USERS)->getForeignEntityType() === 'User' - ) { - return self::ATTR_ASSIGNED_USERS_IDS; - } - - if ($defs->hasAttribute(self::ATTR_ASSIGNED_USER_ID)) { - return self::ATTR_ASSIGNED_USER_ID; - } - - if ($defs->hasAttribute(self::ATTR_CREATED_BY_ID)) { - return self::ATTR_CREATED_BY_ID; - } - - return null; - } - /** * @deprecated Use `$this->config`. */ diff --git a/application/Espo/Core/Acl/EntityAcl.php b/application/Espo/Core/Acl/EntityAcl.php index 85086787b9..c6250e8e26 100644 --- a/application/Espo/Core/Acl/EntityAcl.php +++ b/application/Espo/Core/Acl/EntityAcl.php @@ -33,7 +33,7 @@ use Espo\ORM\Entity; use Espo\Entities\User; -interface EntityAcl +interface EntityAcl extends ScopeAcl { /** * Check access to an entity. @@ -44,10 +44,4 @@ interface EntityAcl * Get an access level for a specific action. */ public function getLevel(User $user, ScopeData $data, string $action) : string; - - /** - * Get an entity attribute that stores an ID (or IDs) of an owner-user. - * NULL means no owner. - */ - public function getOwnerUserIdAttribute(): ?string; } diff --git a/application/Espo/Core/Acl/OwnerUserFieldProvider.php b/application/Espo/Core/Acl/OwnerUserFieldProvider.php new file mode 100644 index 0000000000..35cbe591a3 --- /dev/null +++ b/application/Espo/Core/Acl/OwnerUserFieldProvider.php @@ -0,0 +1,93 @@ +ormDefs = $ormDefs; + $this->metadata = $metadata; + } + + public function get(string $entityType) : ?string + { + $value = $this->metadata->get(['entityAcl', $entityType, 'readOwnerUserField']); + + if ($value) { + return $value; + } + + $defs = $this->ormDefs->getEntity($entityType); + + if ( + $defs->hasField(self::FIELD_ASSIGNED_USERS) && + $defs->getField(self::FIELD_ASSIGNED_USERS)->getType() === 'linkMultiple' && + $defs->hasRelation(self::FIELD_ASSIGNED_USERS) && + $defs->getRelation(self::FIELD_ASSIGNED_USERS)->getForeignEntityType() === 'User' + ) { + return self::FIELD_ASSIGNED_USERS; + } + + if ( + $defs->hasField(self::FIELD_ASSIGNED_USER) && + $defs->getField(self::FIELD_ASSIGNED_USER)->getType() === 'link' && + $defs->hasRelation(self::FIELD_ASSIGNED_USER) && + $defs->getRelation(self::FIELD_ASSIGNED_USER)->getForeignEntityType() === 'User' + ) { + return self::FIELD_ASSIGNED_USER; + } + + if ( + $defs->hasField(self::FIELD_CREATED_BY) && + $defs->getField(self::FIELD_CREATED_BY)->getType() === 'link' && + $defs->hasRelation(self::FIELD_CREATED_BY) && + $defs->getRelation(self::FIELD_CREATED_BY)->getForeignEntityType() === 'User' + ) { + return self::FIELD_CREATED_BY; + } + + return null; + } +} diff --git a/application/Espo/Core/AclManager.php b/application/Espo/Core/AclManager.php index dd6f22a9a4..79632eb307 100644 --- a/application/Espo/Core/AclManager.php +++ b/application/Espo/Core/AclManager.php @@ -38,6 +38,7 @@ use Espo\Core\{ Acl\AclFactory, Acl\GlobalRestrictonFactory, Acl\GlobalRestricton, + Acl\OwnerUserFieldProvider, Acl as UserAclWrapper, Acl\Table as Table, Acl\ScopeAcl, @@ -83,15 +84,19 @@ class AclManager protected $globalRestricton; + protected $ownerUserFieldProvider; + public function __construct( InjectableFactory $injectableFactory, EntityManager $entityManager, AclFactory $aclFactory, - GlobalRestrictonFactory $globalRestrictonFactory + GlobalRestrictonFactory $globalRestrictonFactory, + OwnerUserFieldProvider $ownerUserFieldProvider ) { $this->injectableFactory = $injectableFactory; $this->entityManager = $entityManager; $this->aclFactory = $aclFactory; + $this->ownerUserFieldProvider = $ownerUserFieldProvider; $this->globalRestricton = $globalRestrictonFactory->create(); } @@ -635,4 +640,13 @@ class AclManager return $this->globalRestricton->getScopeRestrictedLinkList($scope, $type); } + + /** + * Get an entity field that stores an owner-user (or multiple users). + * Must be link or linkMulitple field. NULL means no owner. + */ + public function getReadOwnerUserField(string $entityType): ?string + { + return $this->ownerUserFieldProvider->get($entityType); + } } diff --git a/application/Espo/Core/Portal/AclManager.php b/application/Espo/Core/Portal/AclManager.php index 9f0e265ede..4df89d24dc 100644 --- a/application/Espo/Core/Portal/AclManager.php +++ b/application/Espo/Core/Portal/AclManager.php @@ -42,6 +42,7 @@ use Espo\Core\{ AclPortal\AclFactory as PortalAclFactory, Acl\ScopeAcl, Acl\GlobalRestrictonFactory, + Acl\OwnerUserFieldProvider, Acl\Table as TableBase, Portal\Acl as UserAclWrapper, AclManager as BaseAclManager, @@ -69,11 +70,13 @@ class AclManager extends BaseAclManager EntityManager $entityManager, PortalAclFactory $aclFactory, GlobalRestrictonFactory $globalRestrictonFactory, + OwnerUserFieldProvider $ownerUserFieldProvider, BaseAclManager $mainManager ) { $this->injectableFactory = $injectableFactory; $this->entityManager = $entityManager; $this->aclFactory = $aclFactory; + $this->ownerUserFieldProvider = $ownerUserFieldProvider; $this->mainManager = $mainManager; $this->globalRestricton = $globalRestrictonFactory->create(); diff --git a/application/Espo/Hooks/Common/StreamNotesAcl.php b/application/Espo/Hooks/Common/StreamNotesAcl.php index 6fba91e74a..aefc67dafa 100644 --- a/application/Espo/Hooks/Common/StreamNotesAcl.php +++ b/application/Espo/Hooks/Common/StreamNotesAcl.php @@ -50,11 +50,21 @@ class StreamNotesAcl public function afterSave(Entity $entity, array $options = []) { - if (!empty($options['noStream'])) return; - if (!empty($options['silent'])) return; - if (!empty($options['skipStreamNotesAcl'])) return; + if (!empty($options['noStream'])) { + return; + } - if ($entity->isNew()) return; + if (!empty($options['silent'])) { + return; + } + + if (!empty($options['skipStreamNotesAcl'])) { + return; + } + + if ($entity->isNew()) { + return; + } if (!$this->noteService) { $this->noteService = $this->serviceFactory->create('Note'); diff --git a/application/Espo/Hooks/Note/Notifications.php b/application/Espo/Hooks/Note/Notifications.php index 186ecccf49..0a5b9627ac 100644 --- a/application/Espo/Hooks/Note/Notifications.php +++ b/application/Espo/Hooks/Note/Notifications.php @@ -40,6 +40,8 @@ use Espo\Core\{ use Espo\Entities\User; +use StdClass; + class Notifications { protected $notificationService = null; @@ -49,9 +51,13 @@ class Notifications public static $order = 14; protected $metadata; + protected $entityManager; + protected $serviceFactory; + protected $user; + protected $internalAclManager; public function __construct( @@ -72,12 +78,15 @@ class Notifications { $mentionedUserList = []; $data = $entity->get('data'); - if (($data instanceof \StdClass) && ($data->mentions instanceof \StdClass)) { + + if (($data instanceof StdClass) && ($data->mentions instanceof StdClass)) { $mentions = get_object_vars($data->mentions); + foreach ($mentions as $d) { $mentionedUserList[] = $d->id; } } + return $mentionedUserList; } @@ -100,16 +109,27 @@ class Notifications $notifyUserIdList = []; if ($parentType && $parentId) { - $userList = $this->getSubscriberList($parentType, $parentId, $entity->get('isInternal')); + $userList = $this->getSubscriberList($parentType, $parentId, $entity->get('isInternal')); + $userIdMetList = []; + foreach ($userList as $user) { $userIdMetList[] = $user->id; } + if ($superParentType && $superParentId) { - $additionalUserList = $this->getSubscriberList($superParentType, $superParentId, $entity->get('isInternal')); + $additionalUserList = $this + ->getSubscriberList($superParentType, $superParentId, $entity->get('isInternal')); + foreach ($additionalUserList as $user) { - if ($user->isPortal()) continue; - if (in_array($user->id, $userIdMetList)) continue; + if ($user->isPortal()) { + continue; + } + + if (in_array($user->id, $userIdMetList)) { + continue; + } + $userIdMetList[] = $user->id; $userList[] = $user; } @@ -117,14 +137,17 @@ class Notifications if ($entity->get('relatedType')) { $targetType = $entity->get('relatedType'); - } else { + } + else { $targetType = $parentType; } $skipAclCheck = false; + if (!$entity->isAclProcessed()) { $skipAclCheck = true; - } else { + } + else { $teamIdList = $entity->getLinkMultipleIdList('teams'); $userIdList = $entity->getLinkMultipleIdList('users'); } @@ -132,19 +155,24 @@ class Notifications foreach ($userList as $user) { if ($skipAclCheck) { $notifyUserIdList[] = $user->id; + continue; } + if ($user->isAdmin()) { $notifyUserIdList[] = $user->id; + continue; } if ($user->isPortal()) { if ($entity->get('relatedType')) { continue; - } else { + } + else { $notifyUserIdList[] = $user->id; } + continue; } @@ -152,26 +180,34 @@ class Notifications if ($level === 'all') { $notifyUserIdList[] = $user->id; + continue; - } else if ($level === 'team') { + } + else if ($level === 'team') { if (in_array($user->id, $userIdList)) { $notifyUserIdList[] = $user->id; + continue; } if (!empty($teamIdList)) { $userTeamIdList = $user->getLinkMultipleIdList('teams'); + foreach ($teamIdList as $teamId) { if (in_array($teamId, $userTeamIdList)) { $notifyUserIdList[] = $user->id; + break; } } } + continue; + } else if ($level === 'own') { if (in_array($user->id, $userIdList)) { $notifyUserIdList[] = $user->id; + continue; } } @@ -179,60 +215,104 @@ class Notifications } else { $targetType = $entity->get('targetType'); + if ($targetType === 'users') { $targetUserIdList = $entity->get('usersIds'); + if (is_array($targetUserIdList)) { foreach ($targetUserIdList as $userId) { - if ($userId === $this->user->id) continue; - if (in_array($userId, $notifyUserIdList)) continue; + if ($userId === $this->user->id) { + continue; + } + + if (in_array($userId, $notifyUserIdList)) { + continue; + } + $notifyUserIdList[] = $userId; } } - } else if ($targetType === 'teams') { + } + else if ($targetType === 'teams') { $targetTeamIdList = $entity->get('teamsIds'); + if (is_array($targetTeamIdList)) { foreach ($targetTeamIdList as $teamId) { $team = $this->entityManager->getEntity('Team', $teamId); - if (!$team) continue; - $targetUserList = $this->entityManager->getRepository('Team')->findRelated($team, 'users', array( - 'whereClause' => array( - 'isActive' => true - ) - )); + + if (!$team) { + continue; + } + + $targetUserList = $this->entityManager + ->getRDBRepository('Team') + ->getRelation($team, 'users') + ->where([ + 'isActive' => true, + ]) + ->find(); + foreach ($targetUserList as $user) { - if ($user->id === $this->user->id) continue; - if (in_array($user->id, $notifyUserIdList)) continue; + if ($user->id === $this->user->id) { + continue; + } + + if (in_array($user->id, $notifyUserIdList)) { + continue; + } + $notifyUserIdList[] = $user->id; } } } - } else if ($targetType === 'portals') { + } + else if ($targetType === 'portals') { $targetPortalIdList = $entity->get('portalsIds'); + if (is_array($targetPortalIdList)) { foreach ($targetPortalIdList as $portalId) { $portal = $this->entityManager->getEntity('Portal', $portalId); - if (!$portal) continue; - $targetUserList = $this->entityManager->getRepository('Portal')->findRelated($portal, 'users', array( - 'whereClause' => array( - 'isActive' => true - ) - )); + + if (!$portal) { + continue; + } + + $targetUserList = $this->entityManager + ->getRDBRepository('Portal') + ->getRelation($portal, 'users') + ->where([ + 'isActive' => true, + ]) + ->find(); + foreach ($targetUserList as $user) { - if ($user->id === $this->user->id) continue; - if (in_array($user->id, $notifyUserIdList)) continue; + if ($user->id === $this->user->id) { + continue; + } + + if (in_array($user->id, $notifyUserIdList)) { + continue; + } + $notifyUserIdList[] = $user->id; } } } - } else if ($targetType === 'all') { - $targetUserList = $this->entityManager->getRepository('User')->find([ - 'whereClause' => [ + } + else if ($targetType === 'all') { + $targetUserList = $this->entityManager + ->getRepository('User') + ->where([ 'isActive' => true, - 'type' => ['regular', 'admin'] - ] - ]); + 'type' => ['regular', 'admin'], + ]) + ->find(); + foreach ($targetUserList as $user) { - if ($user->id === $this->user->id) continue; + if ($user->id === $this->user->id) { + continue; + } + $notifyUserIdList[] = $user->id; } } @@ -243,18 +323,25 @@ class Notifications foreach ($notifyUserIdList as $i => $userId) { if ($entity->isUserIdNotified($userId)) { unset($notifyUserIdList[$i]); + continue; } + if (!$entity->isNew()) { if ( - $this->entityManager->getRepository('Notification')->select(['id'])->where([ - 'type' => 'Note', - 'relatedType' => 'Note', - 'relatedId' => $entity->id, - 'userId' => $userId, - ])->findOne() + $this->entityManager + ->getRepository('Notification') + ->select(['id']) + ->where([ + 'type' => 'Note', + 'relatedType' => 'Note', + 'relatedId' => $entity->id, + 'userId' => $userId, + ]) + ->findOne() ) { unset($notifyUserIdList[$i]); + continue; } } diff --git a/application/Espo/Modules/Crm/Acl/Meeting.php b/application/Espo/Modules/Crm/Acl/Meeting.php index adb3ec787e..85e344534e 100644 --- a/application/Espo/Modules/Crm/Acl/Meeting.php +++ b/application/Espo/Modules/Crm/Acl/Meeting.php @@ -42,8 +42,6 @@ use Espo\Core\{ class Meeting extends Acl implements EntityReadAcl { - protected $ownerUserIdAttribute = 'usersIds'; - public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool { if ($this->checkEntity($user, $entity, $data, Table::ACTION_READ)) { diff --git a/application/Espo/Modules/Crm/Resources/metadata/entityAcl/Call.json b/application/Espo/Modules/Crm/Resources/metadata/entityAcl/Call.json new file mode 100644 index 0000000000..84c6db81df --- /dev/null +++ b/application/Espo/Modules/Crm/Resources/metadata/entityAcl/Call.json @@ -0,0 +1,3 @@ +{ + "readOwnerUserField": "users" +} diff --git a/application/Espo/Modules/Crm/Resources/metadata/entityAcl/Meeting.json b/application/Espo/Modules/Crm/Resources/metadata/entityAcl/Meeting.json new file mode 100644 index 0000000000..84c6db81df --- /dev/null +++ b/application/Espo/Modules/Crm/Resources/metadata/entityAcl/Meeting.json @@ -0,0 +1,3 @@ +{ + "readOwnerUserField": "users" +} diff --git a/application/Espo/Resources/metadata/entityAcl/Email.json b/application/Espo/Resources/metadata/entityAcl/Email.json index 8d816e7df0..bd2cda920a 100644 --- a/application/Espo/Resources/metadata/entityAcl/Email.json +++ b/application/Espo/Resources/metadata/entityAcl/Email.json @@ -8,5 +8,6 @@ "users": { "onlyAdmin": true } - } + }, + "readOwnerUserField": "users" } diff --git a/application/Espo/Services/Note.php b/application/Espo/Services/Note.php index af661aa10a..12f69c8c08 100644 --- a/application/Espo/Services/Note.php +++ b/application/Espo/Services/Note.php @@ -41,8 +41,8 @@ use Espo\Entities\Note as NoteEntity; use Espo\ORM\Entity; use DateTime; -use Exception; use StdClass; +use Exception; class Note extends Record { @@ -104,9 +104,9 @@ class Note extends Record $parentId = $data->parentId ?? null; if ($parentType && $parentId) { - $entity = $this->entityManager->getEntity($data->parentType, $data->parentId); + $parent = $this->entityManager->getEntity($data->parentType, $data->parentId); - if ($entity && !$this->acl->check($entity, AclTable::ACTION_READ)) { + if ($parent && !$this->acl->check($parent, AclTable::ACTION_READ)) { throw new Forbidden(); } } @@ -372,6 +372,9 @@ class Note extends Record } } + /** + * Changes users and teams of notes related to an entity according users and teams of the entity. + */ public function processNoteAcl(Entity $entity, bool $forceProcessNoteNotifications = false): void { $entityType = $entity->getEntityType(); @@ -388,33 +391,45 @@ class Note extends Record return; } - $ownerUserIdAttribute = $this->getAclManager() - ->getImplementation($entityType) - ->getOwnerUserIdAttribute(); - $usersAttributeIsChanged = false; $teamsAttributeIsChanged = false; - if ($ownerUserIdAttribute) { + $ownerUserField = $this->getAclManager()->getReadOwnerUserField($entityType); + + /* @var $defs \Espo\ORM\Defs\EntityDefs */ + $defs = $this->entityManager->getDefs()->getEntity($entity->getEntityType()); + + $userIdList = []; + + if ($ownerUserField) { + if (!$defs->hasField($ownerUserField)) { + throw new Error("Non-existing read-owner user field."); + } + + $fieldDefs = $defs->getField($ownerUserField); + + if ($fieldDefs->getType() === 'linkMultiple') { + $ownerUserIdAttribute = $ownerUserField . 'Ids'; + } + else if ($fieldDefs->getType() === 'link') { + $ownerUserIdAttribute = $ownerUserField . 'Id'; + } + else { + throw new Error("Bad read-owner user field type."); + } + if ($entity->isAttributeChanged($ownerUserIdAttribute)) { $usersAttributeIsChanged = true; } if ($usersAttributeIsChanged || $forceProcessNoteNotifications) { - if ($entity->getAttributeParam($ownerUserIdAttribute, 'isLinkMultipleIdList')) { - $userLink = $entity->getAttributeParam($ownerUserIdAttribute, 'relation'); - - $userIdList = $entity->getLinkMultipleIdList($userLink); + if ($fieldDefs->getType() === 'linkMultiple') { + $userIdList = $entity->getLinkMultipleIdList($ownerUserField); } else { $userId = $entity->get($ownerUserIdAttribute); - if ($userId) { - $userIdList = [$userId]; - } - else { - $userIdList = []; - } + $userIdList = $userId ? [$userId] : []; } } } @@ -429,70 +444,98 @@ class Note extends Record } } - if ($usersAttributeIsChanged || $teamsAttributeIsChanged || $forceProcessNoteNotifications) { - $noteList = $this->entityManager - ->getRepository('Note') - ->where([ - 'OR' => [ - [ - 'relatedId' => $entity->id, - 'relatedType' => $entityType - ], - [ - 'parentId' => $entity->id, - 'parentType' => $entityType, - 'superParentId!=' => null, - 'relatedId' => null, - ] + if (!$usersAttributeIsChanged && !$teamsAttributeIsChanged && !$forceProcessNoteNotifications) { + return; + } + + $noteList = $this->entityManager + ->getRepository('Note') + ->where([ + 'OR' => [ + [ + 'relatedId' => $entity->getId(), + 'relatedType' => $entityType, + ], + [ + 'parentId' => $entity->getId(), + 'parentType' => $entityType, + 'superParentId!=' => null, + 'relatedId' => null, ] - ]) - ->select([ - 'id', - 'parentType', - 'parentId', - 'superParentType', - 'superParentId', - 'isInternal', - 'relatedType', - 'relatedId', - 'createdAt', - ]) - ->find(); + ] + ]) + ->select([ + 'id', + 'parentType', + 'parentId', + 'superParentType', + 'superParentId', + 'isInternal', + 'relatedType', + 'relatedId', + 'createdAt', + ]) + ->find(); - $noteOptions = []; + $noteOptions = []; - if (!empty($forceProcessNoteNotifications)) { - $noteOptions['forceProcessNotifications'] = true; - } + if (!empty($forceProcessNoteNotifications)) { + $noteOptions['forceProcessNotifications'] = true; + } - $period = '-' . $this->getConfig()->get('noteNotificationPeriod', $this->noteNotificationPeriod); + $period = '-' . $this->getConfig()->get('noteNotificationPeriod', $this->noteNotificationPeriod); - $threshold = new DateTime(); + $threshold = (new DateTime())->modify($period); - $threshold->modify($period); - foreach ($noteList as $note) { - if (!$entity->isNew()) { - try { - $createdAtDt = new DateTime($note->get('createdAt')); - - if ($createdAtDt->getTimestamp() < $threshold->getTimestamp()) { - continue; - } - } - catch (Exception $e) {}; - } - - if ($teamsAttributeIsChanged || $forceProcessNoteNotifications) { - $note->set('teamsIds', $teamIdList); - } - - if ($usersAttributeIsChanged || $forceProcessNoteNotifications) { - $note->set('usersIds', $userIdList); - } - - $this->entityManager->saveEntity($note, $noteOptions); - } + foreach ($noteList as $note) { + $this->processNoteAclItem($entity, $note, [ + 'teamsAttributeIsChanged' => $teamsAttributeIsChanged, + 'usersAttributeIsChanged' => $usersAttributeIsChanged, + 'forceProcessNoteNotifications' => $forceProcessNoteNotifications, + 'teamIdList' => $teamIdList, + 'userIdList' => $userIdList, + 'threshold' => $threshold, + ]); } } + + protected function processNoteAclItem(Entity $entity, NoteEntity $note, array $params): void + { + $teamsAttributeIsChanged = $params['teamsAttributeIsChanged']; + $usersAttributeIsChanged = $params['usersAttributeIsChanged']; + $forceProcessNoteNotifications = $params['forceProcessNoteNotifications']; + + $teamIdList = $params['teamIdList']; + $userIdList = $params['userIdList']; + + $threshold = $params['threshold']; + + $noteOptions = [ + 'forceProcessNotifications' => $forceProcessNoteNotifications, + ]; + + if (!$entity->isNew() && $note->get('createdAt')) { + try { + $createdAtDt = new DateTime($note->get('createdAt')); + } + catch (Exception $e) { + return; + } + + if ($createdAtDt->getTimestamp() < $threshold->getTimestamp()) { + return; + } + } + + if ($teamsAttributeIsChanged || $forceProcessNoteNotifications) { + $note->set('teamsIds', $teamIdList); + } + + if ($usersAttributeIsChanged || $forceProcessNoteNotifications) { + $note->set('usersIds', $userIdList); + } + + $this->entityManager->saveEntity($note, $noteOptions); + } } diff --git a/application/Espo/Services/Stream.php b/application/Espo/Services/Stream.php index a52fd90ff0..b18742cac8 100644 --- a/application/Espo/Services/Stream.php +++ b/application/Espo/Services/Stream.php @@ -179,6 +179,7 @@ class Stream if (!$user) { unset($userIdList[$i]); + continue; } @@ -223,7 +224,8 @@ class Stream $userId = $this->user->id; } - $isFollowed = (bool) $this->entityManager->getRepository('Subscription') + $isFollowed = (bool) $this->entityManager + ->getRepository('Subscription') ->select(['id']) ->where([ 'userId' => $userId, @@ -1223,21 +1225,49 @@ class Stream } } - $ownerUserIdAttribute = $this->aclManager - ->getImplementation($entity->getEntityType()) - ->getOwnerUserIdAttribute(); + $ownerUserField = $this->aclManager->getReadOwnerUserField($entity->getEntityType()); - if ($ownerUserIdAttribute && $entity->get($ownerUserIdAttribute)) { - if ($entity->getAttributeParam($ownerUserIdAttribute, 'isLinkMultipleIdList')) { - $userIdList = $entity->get($ownerUserIdAttribute); - } - else { - $userId = $entity->get($ownerUserIdAttribute); - $userIdList = [$userId]; - } - - $note->set('usersIds', $userIdList); + if (!$ownerUserField) { + return; } + + /* @var $defs \Espo\ORM\Defs\EntityDefs */ + $defs = $this->entityManager->getDefs()->getEntity($entity->getEntityType()); + + if (!$defs->hasField($ownerUserField)) { + return; + } + + $fieldDefs = $defs->getField($ownerUserField); + + if ($fieldDefs->getType() === 'linkMultiple') { + $ownerUserIdAttribute = $ownerUserField . 'Ids'; + } + else if ($fieldDefs->getType() === 'link') { + $ownerUserIdAttribute = $ownerUserField . 'Id'; + } + else { + return; + } + + if (!$entity->has($ownerUserIdAttribute)) { + return; + } + + if ($fieldDefs->getType() === 'linkMultiple') { + $userIdList = $entity->getLinkMultipleIdList($ownerUserField); + } + else { + $userId = $entity->get($ownerUserIdAttribute); + + if (!$userId) { + return; + } + + $userIdList = [$userId]; + } + + $note->set('usersIds', $userIdList); } public function noteEmailReceived(Entity $entity, Entity $email, $isInitial = false): void @@ -1379,6 +1409,7 @@ class Stream $note->set('superParentId', $entity->get('accountId')); $note->set('superParentType', 'Account'); + // only if has super parent $this->processNoteTeamsUsers($note, $entity); } @@ -1527,6 +1558,7 @@ class Stream $note->set('superParentId', $entity->get('accountId')); $note->set('superParentType', 'Account'); + // only if has super parent $this->processNoteTeamsUsers($note, $entity); } @@ -1839,7 +1871,7 @@ class Stream } if ( - $this->aclManager->getLevel($user, $scope, 'read') === 'team' + $this->aclManager->checkReadOnlyTeam($user, $scope) ) { $list[] = $scope; } @@ -1872,7 +1904,7 @@ class Stream } if ( - $this->aclManager->getLevel($user, $scope, 'read') === 'own' + $this->aclManager->checkReadOnlyOwn($user, $scope) ) { $list[] = $scope; } diff --git a/tests/integration/Espo/Core/Acl/AclTest.php b/tests/integration/Espo/Core/Acl/AclTest.php index ed4155a95b..757d55c3b3 100644 --- a/tests/integration/Espo/Core/Acl/AclTest.php +++ b/tests/integration/Espo/Core/Acl/AclTest.php @@ -35,24 +35,29 @@ use Espo\Core\{ class AclTest extends \tests\integration\Core\BaseTestCase { - public function testGetOwnerUserIdAttribute() + public function testGetReadOwnerUserField() { /* @var $aclManager AclManager */ $aclManager = $this->getContainer()->get('aclManager'); $this->assertEquals( - 'assignedUserId', - $aclManager->getImplementation('Account')->getOwnerUserIdAttribute() + 'assignedUser', + $aclManager->getReadOwnerUserField('Account') ); $this->assertEquals( - 'createdById', - $aclManager->getImplementation('Import')->getOwnerUserIdAttribute() + 'createdBy', + $aclManager->getReadOwnerUserField('Import') ); $this->assertEquals( - 'usersIds', - $aclManager->getImplementation('Email')->getOwnerUserIdAttribute() + 'users', + $aclManager->getReadOwnerUserField('Email') + ); + + $this->assertEquals( + 'users', + $aclManager->getReadOwnerUserField('Meeting') ); } } diff --git a/tests/integration/Espo/Note/AclTest.php b/tests/integration/Espo/Note/AclTest.php new file mode 100644 index 0000000000..cab493be7e --- /dev/null +++ b/tests/integration/Espo/Note/AclTest.php @@ -0,0 +1,142 @@ +getContainer()->get('entityManager'); + + /* @var $noteService NoteService*/ + $noteService = $this->getContainer()->get('serviceFactory')->create('Note'); + + /* @var $streamService StreamService*/ + $streamService = $this->getContainer()->get('serviceFactory')->create('Stream'); + + $team1 = $em->createEntity('Team', [ + 'name' => 'team-1', + ]); + + $team2 = $em->createEntity('Team', [ + 'name' => 'team-2', + ]); + + $user1 = $em->createEntity('User', [ + 'userName' => 'user-1', + 'lastName' => 'user-1', + ]); + + $user2 = $em->createEntity('User', [ + 'userName' => 'user-2', + 'lastName' => 'user-2', + ]); + + $account = $em->createEntity('Account', [ + + ]); + + // Opportunity + + $opportunity = $em->createEntity('Opportunity', [ + 'assignedUserId' => $user1->getId(), + 'teamsIds' => [$team1->getId()], + 'accountId' => $account->getId(), + ]); + + $streamService->noteCreate($opportunity); + + $note1 = $em + ->getRDBRepository('Note') + ->where([ + 'type' => 'Create', + 'parentId' => $opportunity->getId(), + 'parentType' => $opportunity->getEntityType(), + ]) + ->findOne(); + + $this->assertEquals([$team1->getId()], $note1->getLinkMultipleIdList('teams')); + $this->assertEquals([$user1->getId()], $note1->getLinkMultipleIdList('users')); + + $opportunity->set([ + 'assignedUserId' => $user2->getId(), + 'teamsIds' => [$team2->getId()], + ]); + + $em->saveEntity($opportunity); + + $note1 = $em->getEntity('Note', $note1->getId()); + + $this->assertEquals([$team2->getId()], $note1->getLinkMultipleIdList('teams')); + $this->assertEquals([$user2->getId()], $note1->getLinkMultipleIdList('users')); + + // Meeting + + $meeting = $em->createEntity('Meeting', [ + 'usersIds' => [$user1->getId()], + 'teamsIds' => [$team1->getId()], + 'parentId' => $account->getId(), + 'parentType' => $account->getEntityType(), + ]); + + $streamService->noteRelate($meeting, $account->getEntityType(), $account->getId()); + + $note2 = $em + ->getRDBRepository('Note') + ->where([ + 'type' => 'Relate', + 'relatedId' => $meeting->getId(), + 'relatedType' => $meeting->getEntityType(), + ]) + ->findOne(); + + $this->assertEquals([$team1->getId()], $note2->getLinkMultipleIdList('teams')); + $this->assertEquals([$user1->getId()], $note2->getLinkMultipleIdList('users')); + + $meeting->set([ + 'usersIds' => [$user2->getId()], + 'teamsIds' => [$team2->getId()], + ]); + + $em->saveEntity($meeting); + + $note2 = $em->getEntity('Note', $note2->getId()); + + $this->assertEquals([$team2->getId()], $note2->getLinkMultipleIdList('teams')); + $this->assertEquals([$user2->getId()], $note2->getLinkMultipleIdList('users')); + } +}