From c1855e84a974aed4e87630966ab3ab99b558fa8b Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Thu, 25 Apr 2024 15:03:14 +0300 Subject: [PATCH] ref --- .../Core/Mail/Importer/DefaultImporter.php | 3 +- .../Espo/Hooks/Common/StreamNotesAcl.php | 8 +- .../Espo/Tools/Stream/Jobs/ProcessNoteAcl.php | 11 +- .../Tools/Stream/NoteAcl/AccessModifier.php | 100 +++++++ .../Espo/Tools/Stream/NoteAcl/Processor.php | 248 ++++++++++++++++++ application/Espo/Tools/Stream/Service.php | 207 --------------- 6 files changed, 358 insertions(+), 219 deletions(-) create mode 100644 application/Espo/Tools/Stream/NoteAcl/AccessModifier.php create mode 100644 application/Espo/Tools/Stream/NoteAcl/Processor.php diff --git a/application/Espo/Core/Mail/Importer/DefaultImporter.php b/application/Espo/Core/Mail/Importer/DefaultImporter.php index 6a6428d7e8..0e1c29f2a3 100644 --- a/application/Espo/Core/Mail/Importer/DefaultImporter.php +++ b/application/Espo/Core/Mail/Importer/DefaultImporter.php @@ -567,8 +567,7 @@ class DefaultImporter implements Importer ->create() ->setClassName(ProcessNoteAcl::class) ->setData( - JobData - ::create() + JobData::create(['notify' => true]) ->withTargetId($duplicate->getId()) ->withTargetType(Email::ENTITY_TYPE) ) diff --git a/application/Espo/Hooks/Common/StreamNotesAcl.php b/application/Espo/Hooks/Common/StreamNotesAcl.php index 13cac15689..e85dfd87aa 100644 --- a/application/Espo/Hooks/Common/StreamNotesAcl.php +++ b/application/Espo/Hooks/Common/StreamNotesAcl.php @@ -31,7 +31,7 @@ namespace Espo\Hooks\Common; use Espo\Core\ORM\Repository\Option\SaveOption; use Espo\ORM\Entity; -use Espo\Tools\Stream\Service as Service; +use Espo\Tools\Stream\NoteAcl\AccessModifier; /** * Notes having `related` or `superParent` are subjects to access control @@ -46,7 +46,7 @@ class StreamNotesAcl { public static int $order = 10; - public function __construct(private Service $service) + public function __construct(private AccessModifier $processor) {} /** @@ -70,8 +70,6 @@ class StreamNotesAcl return; } - $forceProcessNoteNotifications = !empty($options['forceProcessNoteNotifications']); - - $this->service->processNoteAcl($entity, $forceProcessNoteNotifications); + $this->processor->process($entity); } } diff --git a/application/Espo/Tools/Stream/Jobs/ProcessNoteAcl.php b/application/Espo/Tools/Stream/Jobs/ProcessNoteAcl.php index 57eb41e4ee..94230eb19c 100644 --- a/application/Espo/Tools/Stream/Jobs/ProcessNoteAcl.php +++ b/application/Espo/Tools/Stream/Jobs/ProcessNoteAcl.php @@ -31,14 +31,14 @@ namespace Espo\Tools\Stream\Jobs; use Espo\Core\Job\Job; use Espo\Core\Job\Job\Data; +use Espo\Core\ORM\Entity as CoreEntity; use Espo\ORM\EntityManager; -use Espo\Tools\Stream\Service as Service; +use Espo\Tools\Stream\NoteAcl\Processor; class ProcessNoteAcl implements Job { - public function __construct( - private Service $service, + private Processor $processor, private EntityManager $entityManager ) {} @@ -46,6 +46,7 @@ class ProcessNoteAcl implements Job { $targetType = $data->getTargetType(); $targetId = $data->getTargetId(); + $notify = $data->get('notify') === true; if (!$targetType || !$targetId) { return; @@ -57,10 +58,10 @@ class ProcessNoteAcl implements Job $entity = $this->entityManager->getEntityById($targetType, $targetId); - if (!$entity) { + if (!$entity instanceof CoreEntity) { return; } - $this->service->processNoteAcl($entity, true); + $this->processor->process($entity, $notify); } } diff --git a/application/Espo/Tools/Stream/NoteAcl/AccessModifier.php b/application/Espo/Tools/Stream/NoteAcl/AccessModifier.php new file mode 100644 index 0000000000..612bbcb6b5 --- /dev/null +++ b/application/Espo/Tools/Stream/NoteAcl/AccessModifier.php @@ -0,0 +1,100 @@ +. + * + * 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\NoteAcl; + +use Espo\Core\ORM\Entity as CoreEntity; +use Espo\Core\Utils\Metadata; +use Espo\ORM\Entity; + +/** + * Changes users and teams of notes related to an entity according users and teams of the entity. + * + * Notes having `related` or `superParent` are subjects to access control + * through `users` and `teams` fields. + * + * When users or teams of `related` or `parent` record are changed + * the note record will be changed too. + * + * @internal + * @todo Job to process the rest, after the last ID. + */ +class AccessModifier +{ + /** @var string[] */ + private array $ignoreEntityTypeList = [ + 'Note', + 'User', + 'Team', + 'Role', + 'Portal', + 'PortalRole', + ]; + + public function __construct( + private Metadata $metadata, + private Processor $processor + ) {} + + /** + * @internal + * @param bool $notify Process notifications for notes. + */ + public function process(Entity $entity, bool $notify = false): void + { + if (!$entity instanceof CoreEntity) { + return; + } + + if (!$this->toProcess($entity)) { + return; + } + + $this->processor->process($entity, $notify); + } + + private function toProcess(CoreEntity $entity): bool + { + $entityType = $entity->getEntityType(); + + if (in_array($entityType, $this->ignoreEntityTypeList)) { + return false; + } + + if (!$this->metadata->get(['scopes', $entityType, 'acl'])) { + return false; + } + + if (!$this->metadata->get(['scopes', $entityType, 'object'])) { + return false; + } + + return true; + } +} diff --git a/application/Espo/Tools/Stream/NoteAcl/Processor.php b/application/Espo/Tools/Stream/NoteAcl/Processor.php new file mode 100644 index 0000000000..739a3fabe4 --- /dev/null +++ b/application/Espo/Tools/Stream/NoteAcl/Processor.php @@ -0,0 +1,248 @@ +. + * + * 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\NoteAcl; + +use Espo\Core\AclManager; +use Espo\Core\ORM\Entity as CoreEntity; +use Espo\Core\ORM\Type\FieldType; +use Espo\Core\Utils\Config; +use Espo\Entities\Note; +use Espo\ORM\Collection; +use Espo\ORM\Entity; +use Espo\ORM\EntityManager; +use Espo\ORM\Query\Part\Order; + +use DateTime; +use DateTimeInterface; +use LogicException; + +/** + * @internal + */ +class Processor +{ + /** + * When a record is re-assigned, ACL will be recalculated for related notes + * created within the period. + */ + private const NOTE_ACL_PERIOD = '3 days'; + private const NOTE_ACL_LIMIT = 50; + private const NOTE_NOTIFICATION_PERIOD = '1 hour'; + + public function __construct( + private EntityManager $entityManager, + private Config $config, + private AclManager $aclManager, + ) {} + + public function process(CoreEntity $entity, bool $notify = false): void + { + $entityType = $entity->getEntityType(); + + $usersAttributeIsChanged = false; + $teamsAttributeIsChanged = false; + + $ownerUserField = $this->aclManager->getReadOwnerUserField($entityType); + + $defs = $this->entityManager->getDefs()->getEntity($entity->getEntityType()); + + $userIdList = []; + $teamIdList = []; + + if ($ownerUserField) { + if (!$defs->hasField($ownerUserField)) { + throw new LogicException("Non-existing read-owner user field."); + } + + $fieldDefs = $defs->getField($ownerUserField); + + if ($fieldDefs->getType() === FieldType::LINK_MULTIPLE) { + $ownerUserIdAttribute = $ownerUserField . 'Ids'; + } + else if ($fieldDefs->getType() === FieldType::LINK) { + $ownerUserIdAttribute = $ownerUserField . 'Id'; + } + else { + throw new LogicException("Bad read-owner user field type."); + } + + if ($entity->isAttributeChanged($ownerUserIdAttribute)) { + $usersAttributeIsChanged = true; + } + + if ($usersAttributeIsChanged || $notify) { + if ($fieldDefs->getType() === FieldType::LINK_MULTIPLE) { + $userIdList = $entity->getLinkMultipleIdList($ownerUserField); + } + else { + $userId = $entity->get($ownerUserIdAttribute); + + $userIdList = $userId ? [$userId] : []; + } + } + } + + if ($entity->hasLinkMultipleField('teams')) { + if ($entity->isAttributeChanged('teamsIds')) { + $teamsAttributeIsChanged = true; + } + + if ($teamsAttributeIsChanged || $notify) { + $teamIdList = $entity->getLinkMultipleIdList('teams'); + } + } + + if (!$usersAttributeIsChanged && !$teamsAttributeIsChanged && !$notify) { + return; + } + + $notificationThreshold = $this->getNotificationThreshold(); + $aclThreshold = $this->getAclThreshold(); + + foreach ($this->getNotes($entity) as $note) { + $this->processNoteAclItem($entity, $note, [ + 'teamsAttributeIsChanged' => $teamsAttributeIsChanged, + 'usersAttributeIsChanged' => $usersAttributeIsChanged, + 'notify' => $notify, + 'teamIdList' => $teamIdList, + 'userIdList' => $userIdList, + 'notificationThreshold' => $notificationThreshold, + 'aclThreshold' => $aclThreshold, + ]); + } + } + + /** + * @param array{ + * teamsAttributeIsChanged: bool, + * usersAttributeIsChanged: bool, + * notify: bool, + * teamIdList: string[], + * userIdList: string[], + * notificationThreshold: DateTimeInterface, + * aclThreshold: DateTimeInterface, + * } $params + */ + private function processNoteAclItem(Entity $entity, Note $note, array $params): void + { + $teamsAttributeIsChanged = $params['teamsAttributeIsChanged']; + $usersAttributeIsChanged = $params['usersAttributeIsChanged']; + $notify = $params['notify']; + + $teamIdList = $params['teamIdList']; + $userIdList = $params['userIdList']; + + $notificationThreshold = $params['notificationThreshold']; + $aclThreshold = $params['aclThreshold']; + + $createdAt = $note->getCreatedAt(); + + if (!$createdAt) { + return; + } + + if (!$entity->isNew()) { + if ($createdAt->toTimestamp() < $notificationThreshold->getTimestamp()) { + $notify = false; + } + + if ($createdAt->toTimestamp() < $aclThreshold->getTimestamp()) { + return; + } + } + + if ($teamsAttributeIsChanged || $notify) { + $note->setTeamsIds($teamIdList); + } + + if ($usersAttributeIsChanged || $notify) { + $note->setUsersIds($userIdList); + } + + $this->entityManager->saveEntity($note, [ + 'forceProcessNotifications' => $notify, + ]); + } + + /** + * @return Collection + */ + private function getNotes(CoreEntity $entity): Collection + { + $entityType = $entity->getEntityType(); + $limit = $this->config->get('noteAclLimit', self::NOTE_ACL_LIMIT); + + return $this->entityManager + ->getRDBRepository(Note::ENTITY_TYPE) + ->sth() + ->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', + ]) + ->order('number', Order::DESC) + ->limit(0, $limit) + ->find(); + } + + private function getNotificationThreshold(): DateTime + { + $notificationPeriod = '-' . $this->config->get('noteNotificationPeriod', self::NOTE_NOTIFICATION_PERIOD); + + return (new DateTime())->modify($notificationPeriod); + } + + private function getAclThreshold(): DateTime + { + $aclPeriod = '-' . $this->config->get('noteAclPeriod', self::NOTE_ACL_PERIOD); + + return (new DateTime())->modify($aclPeriod); + } +} diff --git a/application/Espo/Tools/Stream/Service.php b/application/Espo/Tools/Stream/Service.php index 9f66601a74..09c3ba4fb0 100644 --- a/application/Espo/Tools/Stream/Service.php +++ b/application/Espo/Tools/Stream/Service.php @@ -66,9 +66,6 @@ use Espo\Core\Select\SearchParams; use Espo\Core\Utils\Acl\UserAclManagerProvider; use stdClass; -use DateTime; -use DateTimeInterface; -use LogicException; class Service { @@ -105,17 +102,6 @@ class Service */ private $auditedFieldsCache = []; - /** - * When a record is re-assigned, ACL will be recalculated for related notes - * created within the period. - */ - private const NOTE_ACL_PERIOD = '3 days'; - private const NOTE_ACL_LIMIT = 50; - /** - * Not used currently. - */ - private const NOTE_NOTIFICATION_PERIOD = '1 hour'; - public function __construct( private EntityManager $entityManager, private Config $config, @@ -1168,199 +1154,6 @@ class Service ->find(); } - /** - * Changes users and teams of notes related to an entity according users and teams of the entity. - * - * Notes having `related` or `superParent` are subjects to access control - * through `users` and `teams` fields. - * - * When users or teams of `related` or `parent` record are changed - * the note record will be changed too. - * - * @todo Job to process the rest, after the last ID. - */ - public function processNoteAcl(Entity $entity, bool $forceProcessNoteNotifications = false): void - { - if (!$entity instanceof CoreEntity) { - return; - } - - $entityType = $entity->getEntityType(); - - if (in_array($entityType, ['Note', 'User', 'Team', 'Role', 'Portal', 'PortalRole'])) { - return; - } - - if (!$this->metadata->get(['scopes', $entityType, 'acl'])) { - return; - } - - if (!$this->metadata->get(['scopes', $entityType, 'object'])) { - return; - } - - $usersAttributeIsChanged = false; - $teamsAttributeIsChanged = false; - - $ownerUserField = $this->aclManager->getReadOwnerUserField($entityType); - - $defs = $this->entityManager->getDefs()->getEntity($entity->getEntityType()); - - $userIdList = []; - $teamIdList = []; - - if ($ownerUserField) { - if (!$defs->hasField($ownerUserField)) { - throw new LogicException("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 LogicException("Bad read-owner user field type."); - } - - if ($entity->isAttributeChanged($ownerUserIdAttribute)) { - $usersAttributeIsChanged = true; - } - - if ($usersAttributeIsChanged || $forceProcessNoteNotifications) { - if ($fieldDefs->getType() === 'linkMultiple') { - $userIdList = $entity->getLinkMultipleIdList($ownerUserField); - } - else { - $userId = $entity->get($ownerUserIdAttribute); - - $userIdList = $userId ? [$userId] : []; - } - } - } - - if ($entity->hasLinkMultipleField('teams')) { - if ($entity->isAttributeChanged('teamsIds')) { - $teamsAttributeIsChanged = true; - } - - if ($teamsAttributeIsChanged || $forceProcessNoteNotifications) { - $teamIdList = $entity->getLinkMultipleIdList('teams'); - } - } - - if (!$usersAttributeIsChanged && !$teamsAttributeIsChanged && !$forceProcessNoteNotifications) { - return; - } - - $limit = $this->config->get('noteAclLimit', self::NOTE_ACL_LIMIT); - - $noteList = $this->entityManager - ->getRDBRepository(Note::ENTITY_TYPE) - ->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', - ]) - ->order('number', 'DESC') - ->limit(0, $limit) - ->find(); - - $notificationPeriod = '-' . $this->config->get('noteNotificationPeriod', self::NOTE_NOTIFICATION_PERIOD); - $aclPeriod = '-' . $this->config->get('noteAclPeriod', self::NOTE_ACL_PERIOD); - - $notificationThreshold = (new DateTime())->modify($notificationPeriod); - $aclThreshold = (new DateTime())->modify($aclPeriod); - - foreach ($noteList as $note) { - $this->processNoteAclItem($entity, $note, [ - 'teamsAttributeIsChanged' => $teamsAttributeIsChanged, - 'usersAttributeIsChanged' => $usersAttributeIsChanged, - 'forceProcessNoteNotifications' => $forceProcessNoteNotifications, - 'teamIdList' => $teamIdList, - 'userIdList' => $userIdList, - 'notificationThreshold' => $notificationThreshold, - 'aclThreshold' => $aclThreshold, - ]); - } - } - - /** - * @param array{ - * teamsAttributeIsChanged: bool, - * usersAttributeIsChanged: bool, - * forceProcessNoteNotifications: bool, - * teamIdList: string[], - * userIdList: string[], - * notificationThreshold: DateTimeInterface, - * aclThreshold: DateTimeInterface, - * } $params - * @return void - */ - private function processNoteAclItem(Entity $entity, Note $note, array $params): void - { - $teamsAttributeIsChanged = $params['teamsAttributeIsChanged']; - $usersAttributeIsChanged = $params['usersAttributeIsChanged']; - $forceProcessNoteNotifications = $params['forceProcessNoteNotifications']; - - $teamIdList = $params['teamIdList']; - $userIdList = $params['userIdList']; - - $notificationThreshold = $params['notificationThreshold']; - $aclThreshold = $params['aclThreshold']; - - $createdAt = $note->getCreatedAt(); - - if (!$createdAt) { - return; - } - - if (!$entity->isNew()) { - if ($createdAt->toTimestamp() < $notificationThreshold->getTimestamp()) { - $forceProcessNoteNotifications = false; - } - - if ($createdAt->toTimestamp() < $aclThreshold->getTimestamp()) { - return; - } - } - - if ($teamsAttributeIsChanged || $forceProcessNoteNotifications) { - $note->setTeamsIds($teamIdList); - } - - if ($usersAttributeIsChanged || $forceProcessNoteNotifications) { - $note->setUsersIds($userIdList); - } - - $this->entityManager->saveEntity($note, [ - 'forceProcessNotifications' => $forceProcessNoteNotifications, - ]); - } - private function getEmailAddressRepository(): EmailAddressRepository { /** @var EmailAddressRepository */