notification refactoring

This commit is contained in:
Yuri Kuznetsov
2021-09-04 16:59:24 +03:00
parent 632e1ecb5c
commit b19ce60e74
14 changed files with 1110 additions and 831 deletions
@@ -29,21 +29,20 @@
namespace Espo\Classes\AssignmentNotificators;
use Espo\{
Services\Email as EmailService,
Services\Stream as StreamService,
Entities\User,
ORM\Entity,
};
use Espo\Services\Email as EmailService;
use Espo\Services\Services\Stream as StreamService;
use Espo\Core\{
Notification\AssignmentNotificator,
Notification\UserEnabledChecker,
Notification\NotificatorParams,
ORM\EntityManager,
ServiceFactory,
AclManager,
};
use Espo\Core\Notification\AssignmentNotificator;
use Espo\Core\Notification\UserEnabledChecker;
use Espo\Core\Notification\NotificatorParams;
use Espo\Core\ServiceFactory;
use Espo\Core\AclManager;
use Espo\ORM\EntityManager;
use Espo\ORM\Entity;
use Espo\Entities\User;
use Espo\Entities\Notification;
use DateTime;
use Exception;
@@ -101,12 +100,12 @@ class Email implements AssignmentNotificator
return;
}
$dt = null;
try {
$dt = new DateTime($dateSent);
}
catch (Exception $e) {}
catch (Exception $e) {
return;
}
if (!$dt) {
return;
@@ -128,14 +127,14 @@ class Email implements AssignmentNotificator
if (
!in_array($userId, $userIdList) &&
!in_array($userId, $previousUserIdList) &&
$userId != $this->user->id
$userId !== $this->user->getId()
) {
$userIdList[] = $userId;
}
}
$data = [
'emailId' => $entity->id,
'emailId' => $entity->getId(),
'emailName' => $entity->get('name'),
];
@@ -159,13 +158,13 @@ class Email implements AssignmentNotificator
if ($person) {
$data['personEntityType'] = $person->getEntityType();
$data['personEntityName'] = $person->get('name');
$data['personEntityId'] = $person->id;
$data['personEntityId'] = $person->getId();
}
}
$userIdFrom = null;
if ($person && $person->getEntityType() == 'User') {
if ($person && $person->getEntityType() === 'User') {
$userIdFrom = $person->id;
}
@@ -241,48 +240,48 @@ class Email implements AssignmentNotificator
continue;
}
if (
$entity->get('status') == 'Archived' ||
$params->getOption('isBeingImported')
) {
if ($parent) {
if ($this->getStreamService()->checkIsFollowed($parent, $userId)) {
continue;
}
}
$isArchivedOrBeingImported =
$entity->get('status') === 'Archived' ||
$params->getOption('isBeingImported');
if ($account) {
if ($this->getStreamService()->checkIsFollowed($account, $userId)) {
continue;
}
}
}
if (
$this->entityManager
->getRepository('Notification')
->where([
'type' => 'EmailReceived',
'userId' => $userId,
'relatedId' => $entity->id,
'relatedType' => 'Email',
])
->select(['id'])
->findOne()
$isArchivedOrBeingImported &&
$parent &&
$this->getStreamService()->checkIsFollowed($parent, $userId)
) {
continue;
}
$notification = $this->entityManager->getEntity('Notification');
if (
$isArchivedOrBeingImported &&
$account &&
$this->getStreamService()->checkIsFollowed($account, $userId)
) {
continue;
}
$notification->set([
'type' => 'EmailReceived',
$existing = $this->entityManager
->getRepository(Notification::ENTITY_TYPE)
->where([
'type' => Notification::TYPE_EMAIL_RECEIVED,
'userId' => $userId,
'relatedId' => $entity->getId(),
'relatedType' => 'Email',
])
->select(['id'])
->findOne();
if ($existing) {
continue;
}
$this->entityManager->createEntity(Notification::ENTITY_TYPE, [
'type' => Notification::TYPE_EMAIL_RECEIVED,
'userId' => $userId,
'data' => $data,
'relatedId' => $entity->getId(),
'relatedType' => 'Email',
]);
$this->entityManager->saveEntity($notification);
}
}
@@ -30,12 +30,10 @@
namespace Espo\Core\Notification;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\Entities\User;
use Espo\Core\{
ORM\EntityManager,
};
use Espo\Entities\Notification;
class DefaultAssignmentNotificator implements AssignmentNotificator
{
@@ -101,15 +99,15 @@ class DefaultAssignmentNotificator implements AssignmentNotificator
}
}
else {
$isNotSelfAssignment = $assignedUserId !== $this->user->id;
$isNotSelfAssignment = $assignedUserId !== $this->user->getId();
}
if (!$isNotSelfAssignment) {
return;
}
$this->entityManager->createEntity('Notification', [
'type' => 'Assign',
$this->entityManager->createEntity(Notification::ENTITY_TYPE, [
'type' => Notification::TYPE_ASSIGN,
'userId' => $assignedUserId,
'data' => [
'entityType' => $entity->getEntityType(),
+28 -7
View File
@@ -82,6 +82,26 @@ class Note extends Entity
return $this->get('parentId');
}
public function getSuperParentType(): ?string
{
return $this->get('superParentType');
}
public function getSuperParentId(): ?string
{
return $this->get('superParentId');
}
public function getRelatedType(): ?string
{
return $this->get('relatedType');
}
public function getRelatedId(): ?string
{
return $this->get('relatedId');
}
public function getData(): stdClass
{
return $this->get('data') ?? (object) [];
@@ -92,6 +112,11 @@ class Note extends Entity
return (bool) $this->get('isInternal');
}
public function getPost(): ?string
{
return $this->get('post');
}
public function setAclIsProcessed(): void
{
$this->aclIsProcessed = true;
@@ -142,7 +167,7 @@ class Note extends Entity
$this->set('attachmentsTypes', $types);
}
public function addNotifiedUserId($userId)
public function addNotifiedUserId($userId): void
{
$userIdList = $this->get('notifiedUserIdList');
@@ -157,13 +182,9 @@ class Note extends Entity
$this->set('notifiedUserIdList', $userIdList);
}
public function isUserIdNotified($userId)
public function isUserIdNotified($userId): bool
{
$userIdList = $this->get('notifiedUserIdList');
if (!is_array($userIdList)) {
$userIdList = [];
}
$userIdList = $this->get('notifiedUserIdList') ?? [];
return in_array($userId, $userIdList);
}
@@ -32,4 +32,18 @@ namespace Espo\Entities;
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';
}
+14 -153
View File
@@ -29,183 +29,44 @@
namespace Espo\Hooks\Common;
use Espo\{
ORM\Entity,
Services\Stream as StreamService,
};
use Espo\Core\{
Utils\Metadata,
Utils\Config,
ORM\EntityManager,
ServiceFactory,
Notification\AssignmentNotificator,
Notification\AssignmentNotificatorFactory,
Notification\NotificatorParams,
};
use Espo\Entities\User;
use Espo\Tools\Notification\HookProcessor;
use Espo\ORM\Entity;
class Notifications
{
public static $order = 10;
private $notifatorsHash = [];
private $processor;
private $streamService;
private $hasStreamCache = [];
private $metadata;
private $config;
private $entityManager;
private $serviceFactory;
private $notificatorFactory;
private $user;
public function __construct(
Metadata $metadata,
Config $config,
EntityManager $entityManager,
ServiceFactory $serviceFactory,
AssignmentNotificatorFactory $notificatorFactory,
User $user
) {
$this->metadata = $metadata;
$this->config = $config;
$this->entityManager = $entityManager;
$this->serviceFactory = $serviceFactory;
$this->notificatorFactory = $notificatorFactory;
$this->user = $user;
public function __construct(HookProcessor $processor)
{
$this->processor = $processor;
}
public function afterSave(Entity $entity, array $options = []): void
public function afterSave(Entity $entity, array $options): void
{
if (!empty($options['silent']) || !empty($options['noNotifications'])) {
return;
}
$entityType = $entity->getEntityType();
/**
* No need to process assignment notifications for entity types that have Stream enabled.
* Users are notified via Stream notifications.
*/
if ($this->checkHasStream($entityType) && !$entity->hasLinkMultipleField('assignedUsers')) {
return;
}
$assignmentNotificationsEntityList = $this->config->get('assignmentNotificationsEntityList', []);
if (!in_array($entityType, $assignmentNotificationsEntityList)) {
return;
}
$notificator = $this->getNotificator($entityType);
if (!$notificator instanceof AssignmentNotificator) {
// For backward compatiblity.
$notificator->process($entity, $options);
return;
}
$params = NotificatorParams::create()->withRawOptions($options);
$notificator->process($entity, $params);
$this->processor->afterSave($entity, $options);
}
public function beforeRemove(Entity $entity, array $options = []): void
public function beforeRemove(Entity $entity, array $options): void
{
if (!empty($options['silent']) || !empty($options['noNotifications'])) {
return;
}
$entityType = $entity->getEntityType();
if ($this->checkHasStream($entityType)) {
$followersData = $this->getStreamService()->getEntityFollowers($entity);
foreach ($followersData['idList'] as $userId) {
if ($userId === $this->user->id) {
continue;
}
$notification = $this->entityManager->getEntity('Notification');
$notification->set([
'userId' => $userId,
'type' => 'EntityRemoved',
'data' => [
'entityType' => $entity->getEntityType(),
'entityId' => $entity->id,
'entityName' => $entity->get('name'),
'userId' => $this->user->id,
'userName' => $this->user->get('name'),
],
]);
$this->entityManager->saveEntity($notification);
}
}
$this->processor->beforeRemove($entity);
}
public function afterRemove(Entity $entity): void
public function afterRemove(Entity $entity, array $options): void
{
$query = $this->entityManager->getQueryBuilder()
->delete()
->from('Notification')
->where([
'OR' => [
[
'relatedId' => $entity->id,
'relatedType' => $entity->getEntityType(),
],
[
'relatedParentId' => $entity->id,
'relatedParentType' => $entity->getEntityType(),
],
],
])
->build();
$this->entityManager->getQueryExecutor()->execute($query);
}
private function checkHasStream($entityType): bool
{
if (!array_key_exists($entityType, $this->hasStreamCache)) {
$this->hasStreamCache[$entityType] = (bool) $this->metadata->get("scopes.{$entityType}.stream");
if (!empty($options['silent'])) {
return;
}
return $this->hasStreamCache[$entityType];
}
/**
* @return AssignmentNotificator
*/
private function getNotificator(string $entityType): object
{
if (empty($this->notifatorsHash[$entityType])) {
$notificator = $this->notificatorFactory->create($entityType);
$this->notifatorsHash[$entityType] = $notificator;
}
return $this->notifatorsHash[$entityType];
}
private function getStreamService(): StreamService
{
if (empty($this->streamService)) {
$this->streamService = $this->serviceFactory->create('Stream');
}
return $this->streamService;
$this->processor->afterRemove($entity);
}
}
+8 -139
View File
@@ -29,157 +29,26 @@
namespace Espo\Hooks\Note;
use Espo\Tools\Notification\NoteMentionHookProcessor;
use Espo\ORM\Entity;
use Espo\Core\{
ORM\EntityManager,
ServiceFactory,
AclManager,
Acl,
};
use Espo\Entities\User;
class Mentions
{
public static $order = 9;
protected $notificationService = null;
private $processor;
protected $entityManager;
protected $serviceFactory;
protected $user;
protected $aclManager;
protected $acl;
public function __construct(
EntityManager $entityManager,
ServiceFactory $serviceFactory,
User $user,
AclManager $aclManager,
Acl $acl
) {
$this->entityManager = $entityManager;
$this->serviceFactory = $serviceFactory;
$this->user = $user;
$this->aclManager = $aclManager;
$this->acl = $acl;
public function __construct(NoteMentionHookProcessor $processor)
{
$this->processor = $processor;
}
protected function addMentionData($entity)
public function beforeSave(Entity $entity, array $options): void
{
$post = $entity->get('post');
$mentionData = (object) [];
$previousMentionList = [];
if (!$entity->isNew()) {
$data = $entity->get('data');
if (!empty($data) && !empty($data->mentions)) {
$previousMentionList = array_keys(get_object_vars($data->mentions));
}
}
preg_match_all('/(@[\w@.-]+)/', $post, $matches);
$mentionCount = 0;
if (is_array($matches) && !empty($matches[0]) && is_array($matches[0])) {
$parent = null;
if ($entity->get('parentId') && $entity->get('parentType')) {
$parent = $this->entityManager
->getEntity($entity->get('parentType'), $entity->get('parentId'));
}
foreach ($matches[0] as $item) {
$userName = substr($item, 1);
$user = $this->entityManager
->getRepository('User')
->where(['userName' => $userName])
->findOne();
if ($user) {
if (!$this->acl->checkUserPermission($user, 'assignment')) {
continue;
}
$m = [
'id' => $user->id,
'name' => $user->get('name'),
'userName' => $user->get('userName'),
'_scope' => $user->getEntityType(),
];
$mentionData->$item = (object) $m;
$mentionCount++;
if (!in_array($item, $previousMentionList)) {
if ($user->id == $this->user->id) {
continue;
}
$this->notifyAboutMention($entity, $user, $parent);
$entity->addNotifiedUserId($user->id);
}
}
}
}
$data = $entity->get('data');
if (empty($data)) {
$data = (object) [];
}
if ($mentionCount) {
$data->mentions = $mentionData;
}
else {
unset($data->mentions);
}
$entity->set('data', $data);
}
public function beforeSave(Entity $entity)
{
if ($entity->get('type') == 'Post') {
$post = $entity->get('post');
$this->addMentionData($entity);
}
}
protected function notifyAboutMention(Entity $entity, User $user, Entity $parent = null)
{
if ($user->isPortal()) {
if (!empty($options['silent'])) {
return;
}
if ($parent) {
if (!$this->aclManager->check($user, $parent, 'stream')) {
return;
}
}
$this->getNotificationService()->notifyAboutMentionInPost($user->id, $entity->id);
}
protected function getNotificationService()
{
if (empty($this->notificationService)) {
$this->notificationService = $this->serviceFactory->create('Notification');
}
return $this->notificationService;
$this->processor->beforeSave($entity);
}
}
+6 -326
View File
@@ -30,345 +30,25 @@
namespace Espo\Hooks\Note;
use Espo\ORM\Entity;
use Espo\Core\{
ServiceFactory,
AclManager as InternalAclManager,
Utils\Metadata,
ORM\EntityManager,
};
use Espo\Entities\User;
use StdClass;
use Espo\Tools\Notification\NoteHookProcessor;
class Notifications
{
protected $notificationService = null;
protected $streamService = null;
public static $order = 14;
protected $metadata;
private $processor;
protected $entityManager;
protected $serviceFactory;
protected $user;
protected $internalAclManager;
public function __construct(
Metadata $metadata,
EntityManager $entityManager,
ServiceFactory $serviceFactory,
User $user,
InternalAclManager $internalAclManager
) {
$this->metadata = $metadata;
$this->entityManager = $entityManager;
$this->serviceFactory = $serviceFactory;
$this->user = $user;
$this->internalAclManager = $internalAclManager;
}
protected function getMentionedUserIdList($entity)
public function __construct(NoteHookProcessor $processor)
{
$mentionedUserList = [];
$data = $entity->get('data');
if (($data instanceof StdClass) && ($data->mentions instanceof StdClass)) {
$mentions = get_object_vars($data->mentions);
foreach ($mentions as $d) {
$mentionedUserList[] = $d->id;
}
}
return $mentionedUserList;
$this->processor = $processor;
}
protected function getSubscriberList(string $parentType, string $parentId, bool $isInternal = false)
{
return $this->getStreamService()->getSubscriberList($parentType, $parentId, $isInternal);
}
public function afterSave(Entity $entity, array $options = [])
public function afterSave(Entity $entity, array $options): void
{
if (!$entity->isNew() && empty($options['forceProcessNotifications'])) {
return;
}
$parentType = $entity->get('parentType');
$parentId = $entity->get('parentId');
$superParentType = $entity->get('superParentType');
$superParentId = $entity->get('superParentId');
$notifyUserIdList = [];
if ($parentType && $parentId) {
$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'));
foreach ($additionalUserList as $user) {
if ($user->isPortal()) {
continue;
}
if (in_array($user->id, $userIdMetList)) {
continue;
}
$userIdMetList[] = $user->id;
$userList[] = $user;
}
}
if ($entity->get('relatedType')) {
$targetType = $entity->get('relatedType');
}
else {
$targetType = $parentType;
}
$skipAclCheck = false;
if (!$entity->isAclProcessed()) {
$skipAclCheck = true;
}
else {
$teamIdList = $entity->getLinkMultipleIdList('teams');
$userIdList = $entity->getLinkMultipleIdList('users');
}
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 {
$notifyUserIdList[] = $user->id;
}
continue;
}
$level = $this->internalAclManager->getLevel($user, $targetType, 'read');
if ($level === 'all') {
$notifyUserIdList[] = $user->id;
continue;
}
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;
}
}
}
} 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;
}
$notifyUserIdList[] = $userId;
}
}
}
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
->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;
}
$notifyUserIdList[] = $user->id;
}
}
}
}
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
->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;
}
$notifyUserIdList[] = $user->id;
}
}
}
}
else if ($targetType === 'all') {
$targetUserList = $this->entityManager
->getRepository('User')
->where([
'isActive' => true,
'type' => ['regular', 'admin'],
])
->find();
foreach ($targetUserList as $user) {
if ($user->id === $this->user->id) {
continue;
}
$notifyUserIdList[] = $user->id;
}
}
}
$notifyUserIdList = array_unique($notifyUserIdList);
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()
) {
unset($notifyUserIdList[$i]);
continue;
}
}
}
$notifyUserIdList = array_values($notifyUserIdList);
if (!empty($notifyUserIdList)) {
$this->getNotificationService()->notifyAboutNote($notifyUserIdList, $entity);
}
}
protected function getNotificationService()
{
if (empty($this->notificationService)) {
$this->notificationService = $this->serviceFactory->create('Notification');
}
return $this->notificationService;
}
protected function getStreamService()
{
if (empty($this->streamService)) {
$this->streamService = $this->serviceFactory->create('Stream');
}
return $this->streamService;
$this->processor->afterSave($entity);
}
}
+1 -1
View File
@@ -208,7 +208,7 @@ class Note extends Record
$portalIdList = $entity->getLinkMultipleIdList('portals');
$teamIdList = $entity->getLinkMultipleIdList('teams');
$targetUserList = null;
$targetUserList = [];
if ($targetType === NoteEntity::TARGET_USERS) {
$targetUserList = $this->entityManager
+19 -146
View File
@@ -29,177 +29,50 @@
namespace Espo\Services;
use Espo\Core\Utils\Util;
use Espo\Core\{
Record\Collection as RecordCollection,
Exceptions\Error,
};
use Espo\Entities\Note;
use Espo\Entities\User;
use Espo\Services\Stream\NoteAccessControl;
use Espo\Core\Di;
class Notification extends \Espo\Services\Record implements
Di\WebSocketSubmissionAware
class Notification extends \Espo\Services\Record
{
use Di\WebSocketSubmissionSetter;
protected $actionHistoryDisabled = true;
private $noteAccessControl = null;
public function notifyAboutMentionInPost(string $userId, string $noteId): void
{
$notification = $this->entityManager->getEntity('Notification');
$notification->set([
'type' => 'MentionInPost',
'data' => ['noteId' => $noteId],
'userId' => $userId,
'relatedId' => $noteId,
'relatedType' => 'Note',
]);
$this->entityManager->saveEntity($notification);
}
public function notifyAboutNote(array $userIdList, Note $note): void
{
$data = ['noteId' => $note->id];
$related = null;
if ($note->get('relatedType') == 'Email') {
$related = $this->entityManager
->getRepository('Email')
->select(['id', 'sentById', 'createdById'])
->where(['id' => $note->get('relatedId')])
->findOne();
}
$now = date('Y-m-d H:i:s');
$collection = $this->entityManager
->getCollectionFactory()
->create();
$userList = $this->entityManager
->getRepository('User')
->select(['id', 'type'])
->where([
'isActive' => true,
'id' => $userIdList,
])
->find();
foreach ($userList as $user) {
$userId = $user->id;
if (!$this->checkUserNoteAccess($user, $note)) {
continue;
}
if ($note->get('createdById') === $user->id) {
continue;
}
if ($related && $related->getEntityType() == 'Email' && $related->get('sentById') == $user->id) {
continue;
}
if ($related && $related->get('createdById') == $user->id) {
continue;
}
$notification = $this->entityManager->getEntity('Notification');
$notification->set([
'id' => Util::generateId(),
'data' => $data,
'type' => 'Note',
'userId' => $userId,
'createdAt' => $now,
'relatedId' => $note->id,
'relatedType' => 'Note',
'relatedParentId' => $note->get('parentId'),
'relatedParentType' => $note->get('parentType'),
]);
$collection[] = $notification;
}
if (empty($collection)) {
return;
}
$this->entityManager->getMapper()->massInsert($collection);
if ($this->getConfig()->get('useWebSocket')) {
foreach ($userIdList as $userId) {
$this->webSocketSubmission->submit('newNotification', $userId);
}
}
}
public function checkUserNoteAccess(User $user, Note $note): bool
{
if ($user->isPortal()) {
if ($note->get('relatedType')) {
if ($note->get('relatedType') === 'Email' && $note->get('parentType') === 'Case') {
return true;
}
return false;
}
return true;
}
if ($note->get('relatedType')) {
if (!$this->getAclManager()->checkScope($user, $note->get('relatedType'))) {
return false;
}
}
if ($note->get('parentType')) {
if (!$this->getAclManager()->checkScope($user, $note->get('parentType'))) {
return false;
}
}
return true;
}
public function getNotReadCount(string $userId): int
{
$whereClause = array(
$whereClause = [
'userId' => $userId,
'read' => 0
);
'read' => false,
];
$ignoreScopeList = $this->getIgnoreScopeList();
if (!empty($ignoreScopeList)) {
$where = [];
$where[] = array(
'OR' => array(
if (count($ignoreScopeList)) {
$whereClause[] = [
'OR' => [
'relatedParentType' => null,
'relatedParentType!=' => $ignoreScopeList
)
);
$whereClause[] = $where;
'relatedParentType!=' => $ignoreScopeList,
]
];
}
return $this->entityManager->getRepository('Notification')->where($whereClause)->count();
return $this->entityManager
->getRDBRepository('Notification')
->where($whereClause)
->count();
}
public function markAllRead(string $userId)
{
$update = $this->entityManager->getQueryBuilder()->update()
$update = $this->entityManager
->getQueryBuilder()
->update()
->in('Notification')
->set(['read' => true])
->where([
@@ -359,7 +232,7 @@ class Notification extends \Espo\Services\Record implements
return new RecordCollection($collection, $count);
}
protected function getIgnoreScopeList(): array
private function getIgnoreScopeList(): array
{
$ignoreScopeList = [];
@@ -0,0 +1,193 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* 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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Notification;
use Espo\Core\Notification\AssignmentNotificatorFactory;
use Espo\Core\Notification\AssignmentNotificator;
use Espo\Core\Notification\NotificatorParams;
use Espo\Core\Utils\Metadata;
use Espo\Core\Utils\Config;
use Espo\Services\Stream as StreamService;
use Espo\ORM\EntityManager;
use Espo\ORM\Entity;
use Espo\Entities\User;
use Espo\Entities\Notification;
/**
* Handles operations with entities.
*/
class HookProcessor
{
private $notifatorsHash = [];
private $hasStreamCache = [];
private $metadata;
private $config;
private $entityManager;
private $streamService;
private $notificatorFactory;
private $user;
public function __construct(
Metadata $metadata,
Config $config,
EntityManager $entityManager,
StreamService $streamService,
AssignmentNotificatorFactory $notificatorFactory,
User $user
) {
$this->metadata = $metadata;
$this->config = $config;
$this->entityManager = $entityManager;
$this->streamService = $streamService;
$this->notificatorFactory = $notificatorFactory;
$this->user = $user;
}
public function afterSave(Entity $entity, array $options): void
{
$entityType = $entity->getEntityType();
/**
* No need to process assignment notifications for entity types that have Stream enabled.
* Users are notified via Stream notifications.
*/
if ($this->checkHasStream($entityType) && !$entity->hasLinkMultipleField('assignedUsers')) {
return;
}
$assignmentNotificationsEntityList = $this->config->get('assignmentNotificationsEntityList') ?? [];
if (!in_array($entityType, $assignmentNotificationsEntityList)) {
return;
}
$notificator = $this->getNotificator($entityType);
if (!$notificator instanceof AssignmentNotificator) {
// For backward compatiblity.
$notificator->process($entity, $options);
return;
}
$params = NotificatorParams::create()->withRawOptions($options);
$notificator->process($entity, $params);
}
public function beforeRemove(Entity $entity): void
{
$entityType = $entity->getEntityType();
if (!$this->checkHasStream($entityType)) {
return;
}
$followersData = $this->streamService->getEntityFollowers($entity);
$userIdList = $followersData['idList'];
foreach ($userIdList as $userId) {
if ($userId === $this->user->getId()) {
continue;
}
$this->entityManager->createEntity(Notification::ENTITY_TYPE, [
'userId' => $userId,
'type' => Notification::TYPE_ENTITY_REMOVED,
'data' => [
'entityType' => $entity->getEntityType(),
'entityId' => $entity->getId(),
'entityName' => $entity->get('name'),
'userId' => $this->user->getId(),
'userName' => $this->user->get('name'),
],
]);
}
}
public function afterRemove(Entity $entity): void
{
$query = $this->entityManager
->getQueryBuilder()
->delete()
->from(Notification::ENTITY_TYPE)
->where([
'OR' => [
[
'relatedId' => $entity->getId(),
'relatedType' => $entity->getEntityType(),
],
[
'relatedParentId' => $entity->getId(),
'relatedParentType' => $entity->getEntityType(),
],
],
])
->build();
$this->entityManager->getQueryExecutor()->execute($query);
}
private function checkHasStream(string $entityType): bool
{
if (!array_key_exists($entityType, $this->hasStreamCache)) {
$this->hasStreamCache[$entityType] =
(bool) $this->metadata->get(['scopes', $entityType, 'stream']);
}
return $this->hasStreamCache[$entityType];
}
/**
* @return AssignmentNotificator
*/
private function getNotificator(string $entityType): object
{
if (empty($this->notifatorsHash[$entityType])) {
$notificator = $this->notificatorFactory->create($entityType);
$this->notifatorsHash[$entityType] = $notificator;
}
return $this->notifatorsHash[$entityType];
}
}
@@ -0,0 +1,403 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* 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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Notification;
use Espo\Core\Utils\Metadata;
use Espo\Core\AclManager as InternalAclManager;
use Espo\Core\Acl\Table;
use Espo\Services\Stream as StreamService;
use Espo\ORM\EntityManager;
use Espo\ORM\Collection;
use Espo\Entities\User;
use Espo\Entities\Team;
use Espo\Entities\Portal;
use Espo\Entities\Notification;
use Espo\Entities\Note;
/**
* Handles notifications after note saving.
*/
class NoteHookProcessor
{
private $streamService;
private $service;
private $metadata;
private $entityManager;
private $user;
private $internalAclManager;
public function __construct(
StreamService $streamService,
Service $service,
Metadata $metadata,
EntityManager $entityManager,
User $user,
InternalAclManager $internalAclManager
) {
$this->streamService = $streamService;
$this->service = $service;
$this->metadata = $metadata;
$this->entityManager = $entityManager;
$this->user = $user;
$this->internalAclManager = $internalAclManager;
}
public function afterSave(Note $note): void
{
if ($note->getParentType() && $note->getParentId()) {
$this->afterSaveParent($note);
return;
}
$this->afterSaveNoParent($note);
}
private function afterSaveParent(Note $note): void
{
$parentType = $note->getParentType();
$parentId = $note->getParentId();
$superParentType = $note->getSuperParentType();
$superParentId = $note->getSuperParentId();
$userList = $this->getSubscriberList($parentType, $parentId, $note->isInternal());
$userIdMetList = [];
foreach ($userList as $user) {
$userIdMetList[] = $user->getId();
}
if ($superParentType && $superParentId) {
$additionalUserList = $this->getSubscriberList(
$superParentType,
$superParentId,
$note->isInternal()
);
foreach ($additionalUserList as $user) {
if (
$user->isPortal() ||
in_array($user->getId(), $userIdMetList)
) {
continue;
}
$userIdMetList[] = $user->getId();
$userList[] = $user;
}
}
$targetType = $note->getRelatedType() ? $note->getRelatedType() : $parentType;
// This is correct.
$skipAclCheck = !$note->isAclProcessed();
if (!$skipAclCheck) {
$teamIdList = $note->getLinkMultipleIdList('teams');
$userIdList = $note->getLinkMultipleIdList('users');
}
foreach ($userList as $user) {
if ($skipAclCheck) {
$notifyUserIdList[] = $user->getId();
continue;
}
if ($user->isAdmin()) {
$notifyUserIdList[] = $user->getId();
continue;
}
if ($user->isPortal() && $note->getRelatedType()) {
continue;
}
if ($user->isPortal()) {
$notifyUserIdList[] = $user->getId();
continue;
}
$level = $this->internalAclManager->getLevel($user, $targetType, Table::ACTION_READ);
if (!$this->checkUserAccess($user, $level, $teamIdList, $userIdList)) {
continue;
}
$notifyUserIdList[] = $user->getId();
}
$this->processNotify($note, array_unique($notifyUserIdList));
}
private function afterSaveNoParent(Note $note): void
{
$targetType = $note->getTargetType();
if ($targetType === Note::TARGET_USERS) {
$this->afterSaveTargetUsers($note);
return;
}
if ($targetType === Note::TARGET_TEAMS) {
$this->afterSaveTargetTeams($note);
return;
}
if ($targetType === Note::TARGET_PORTALS) {
$this->afterSaveTargetPortals($note);
return;
}
if ($targetType === Note::TARGET_ALL) {
$this->afterSaveTargetAll($note);
return;
}
}
private function processNotify(Note $note, array $userIdList): void
{
$filteredUserIdList = array_filter(
$userIdList,
function (string $userId) use ($note) {
if ($note->isUserIdNotified($userId)) {
return false;
}
if ($note->isNew()) {
return true;
}
$existing = $this->entityManager
->getRDBRepository(Notification::ENTITY_TYPE)
->select(['id'])
->where([
'type' => Notification::TYPE_NOTE,
'relatedType' => Note::ENTITY_TYPE,
'relatedId' => $note->getId(),
'userId' => $userId,
])
->findOne();
if ($existing) {
return false;
}
return true;
}
);
if (!count($filteredUserIdList)) {
return;
}
$this->service->notifyAboutNote($filteredUserIdList, $note);
}
private function afterSaveTargetUsers(Note $note): void
{
$targetUserIdList = $note->get('usersIds') ?? [];
if (!count($targetUserIdList)) {
return;
}
$notifyUserIdList = [];
foreach ($targetUserIdList as $userId) {
if ($userId === $this->user->getId()) {
continue;
}
$notifyUserIdList[] = $userId;
}
$this->processNotify($note, array_unique($notifyUserIdList));
}
private function afterSaveTargetTeams(Note $note): void
{
$targetTeamIdList = $note->get('teamsIds') ?? [];
if (!count($targetTeamIdList)) {
return;
}
foreach ($targetTeamIdList as $teamId) {
$team = $this->entityManager->getEntity(Team::ENTITY_TYPE, $teamId);
if (!$team) {
continue;
}
$targetUserList = $this->entityManager
->getRDBRepository(Team::ENTITY_TYPE)
->getRelation($team, 'users')
->where([
'isActive' => true,
])
->select('id')
->find();
foreach ($targetUserList as $user) {
if ($user->getId() === $this->user->getId()) {
continue;
}
$notifyUserIdList[] = $user->getId();
}
}
$this->processNotify($note, array_unique($notifyUserIdList));
}
private function afterSaveTargetPortals(Note $note): void
{
$targetPortalIdList = $note->get('portalsIds') ?? [];
if (!count($targetPortalIdList)) {
return;
}
$notifyUserIdList = [];
foreach ($targetPortalIdList as $portalId) {
$portal = $this->entityManager->getEntity(Portal::ENTITY_TYPE, $portalId);
if (!$portal) {
continue;
}
$targetUserList = $this->entityManager
->getRDBRepository(Portal::ENTITY_TYPE)
->getRelation($portal, 'users')
->where([
'isActive' => true,
])
->select(['id'])
->find();
foreach ($targetUserList as $user) {
if ($user->getId() === $this->user->getId()) {
continue;
}
$notifyUserIdList[] = $user->getId();
}
}
$this->processNotify($note, array_unique($notifyUserIdList));
}
private function afterSaveTargetAll(Note $note): void
{
$targetUserList = $this->entityManager
->getRDBRepository(User::ENTITY_TYPE)
->where([
'isActive' => true,
'type' => ['regular', 'admin'],
])
->select('id')
->find();
$notifyUserIdList = [];
foreach ($targetUserList as $user) {
if ($user->getId() === $this->user->getId()) {
continue;
}
$notifyUserIdList[] = $user->getId();
}
$this->processNotify($note, $notifyUserIdList);
}
private function checkUserAccess(
User $user,
string $level,
array $teamIdList,
array $userIdList
): bool {
if ($level === Table::LEVEL_ALL) {
return true;
}
if ($level === Table::LEVEL_TEAM) {
if (in_array($user->getId(), $userIdList)) {
return true;
}
if (!count($teamIdList)) {
return false;
}
$userTeamIdList = $user->getLinkMultipleIdList('teams');
foreach ($teamIdList as $teamId) {
if (in_array($teamId, $userTeamIdList)) {
return true;
}
}
return false;
}
if ($level === Table::LEVEL_OWN) {
return in_array($user->getId(), $userIdList);
}
return false;
}
/**
* @return User[]
*/
private function getSubscriberList(string $parentType, string $parentId, bool $isInternal = false): Collection
{
return $this->streamService->getSubscriberList($parentType, $parentId, $isInternal);
}
}
@@ -0,0 +1,181 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* 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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Notification;
use Espo\Core\Acl;
use Espo\Core\AclManager;
use Espo\ORM\EntityManager;
use Espo\Entities\User;
use Espo\Entities\Note;
use stdClass;
class NoteMentionHookProcessor
{
private $service;
private $entityManager;
private $user;
private $acl;
private $aclManager;
public function __construct(
Service $service,
EntityManager $entityManager,
User $user,
Acl $acl,
AclManager $aclManager
) {
$this->service = $service;
$this->entityManager = $entityManager;
$this->user = $user;
$this->acl = $acl;
$this->aclManager = $aclManager;
}
public function beforeSave(Note $note): void
{
if ($note->getType() !== Note::TYPE_POST) {
return;
}
$this->process($note);
}
private function process(Note $note): void
{
$post = $note->getPost() ?? '';
$mentionData = (object) [];
$previousMentionList = [];
if (!$note->isNew()) {
$previousMentionList = array_keys(
get_object_vars($note->getData()->mentions ?? (object) [])
);
}
$matches = null;
preg_match_all('/(@[\w@.-]+)/', $post, $matches);
$mentionCount = 0;
if (is_array($matches) && !empty($matches[0]) && is_array($matches[0])) {
$mentionCount = $this->processMatches($matches[0], $note, $mentionData, $previousMentionList);
}
$data = $note->getData();
if ($mentionCount) {
$data->mentions = $mentionData;
}
else {
unset($data->mentions);
}
$note->set('data', $data);
}
private function processMatches(
array $matchList,
Note $note,
stdClass $mentionData,
array $previousMentionList
): int {
$mentionCount = 0;
$parent = $note->getParentId() && $note->getParentType() ?
$this->entityManager->getEntity(
$note->getParentType(),
$note->getParentId()
) :
null;
foreach ($matchList as $item) {
$userName = substr($item, 1);
/** @var User $user */
$user = $this->entityManager
->getRDBRepository(User::ENTITY_TYPE)
->where([
'userName' => $userName,
'isActive' => true,
])
->findOne();
if (!$user) {
continue;
}
if (!$this->acl->checkUserPermission($user, 'assignment')) {
continue;
}
$mentionData->$item = (object) [
'id' => $user->getId(),
'name' => $user->get('name'),
'userName' => $user->get('userName'),
'_scope' => $user->getEntityType(),
];
$mentionCount++;
if (in_array($item, $previousMentionList)) {
continue;
}
if ($user->getId() === $this->user->getId()) {
continue;
}
if ($user->isPortal()) {
continue;
}
if ($parent && !$this->aclManager->checkEntityStream($user, $parent)) {
continue;
}
$note->addNotifiedUserId($user->getId());
$this->service->notifyAboutMentionInPost($user->getId(), $note);
}
return $mentionCount;
}
}
@@ -0,0 +1,187 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* 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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Notification;
use Espo\Entities\Note;
use Espo\Entities\Notification;
use Espo\Entities\User;
use Espo\Entities\Email;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Util;
use Espo\Core\AclManager;
use Espo\Core\WebSocket\Submission;
use Espo\Core\Utils\DateTime as DateTimeUtil;
use Espo\ORM\EntityManager;
class Service
{
private $entityManager;
private $config;
private $aclManager;
private $webSocketSubmission;
public function __construct(
EntityManager $entityManager,
Config $config,
AclManager $aclManager,
Submission $webSocketSubmission
) {
$this->entityManager = $entityManager;
$this->config = $config;
$this->aclManager = $aclManager;
$this->webSocketSubmission = $webSocketSubmission;
}
public function notifyAboutMentionInPost(string $userId, Note $note): void
{
$this->entityManager->createEntity(Notification::ENTITY_TYPE, [
'type' => Notification::TYPE_MENTION_IN_POST,
'data' => [
'noteId' => $note->getId(),
],
'userId' => $userId,
'relatedId' => $note->getId(),
'relatedType' => Note::ENTITY_TYPE,
]);
}
public function notifyAboutNote(array $userIdList, Note $note): void
{
$related = null;
if ($note->getRelatedType() === Email::ENTITY_TYPE) {
$related = $this->entityManager
->getRDBRepository(Email::ENTITY_TYPE)
->select(['id', 'sentById', 'createdById'])
->where(['id' => $note->getRelatedId()])
->findOne();
}
$now = date(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT);
$collection = $this->entityManager
->getCollectionFactory()
->create();
$userList = $this->entityManager
->getRDBRepository('User')
->select(['id', 'type'])
->where([
'isActive' => true,
'id' => $userIdList,
])
->find();
foreach ($userList as $user) {
$userId = $user->getId();
if (!$this->checkUserNoteAccess($user, $note)) {
continue;
}
if ($note->get('createdById') === $user->getId()) {
continue;
}
if (
$related &&
$related->getEntityType() === Email::ENTITY_TYPE &&
$related->get('sentById') === $user->getId()
) {
continue;
}
if ($related && $related->get('createdById') === $user->getId()) {
continue;
}
$notification = $this->entityManager->getEntity(Notification::ENTITY_TYPE);
$notification->set([
'id' => Util::generateId(),
'data' => [
'noteId' => $note->getId(),
],
'type' => Notification::TYPE_NOTE,
'userId' => $userId,
'createdAt' => $now,
'relatedId' => $note->getId(),
'relatedType' => Note::ENTITY_TYPE,
'relatedParentId' => $note->getParentId(),
'relatedParentType' => $note->getParentType(),
]);
$collection[] = $notification;
}
if (!count($collection)) {
return;
}
$this->entityManager->getMapper()->massInsert($collection);
if ($this->config->get('useWebSocket')) {
foreach ($userIdList as $userId) {
$this->webSocketSubmission->submit('newNotification', $userId);
}
}
}
private function checkUserNoteAccess(User $user, Note $note): bool
{
if ($user->isPortal()) {
if ($note->getRelatedType()) {
/** @todo Revise. */
return $note->getRelatedType() === Email::ENTITY_TYPE && $note->getParentType() === 'Case';
}
return true;
}
if ($note->getRelatedType()) {
if (!$this->aclManager->checkScope($user, $note->getRelatedType())) {
return false;
}
}
if ($note->getParentType()) {
if (!$this->aclManager->checkScope($user, $note->getParentType())) {
return false;
}
}
return true;
}
}
@@ -38,7 +38,7 @@ use Espo\Core\Acl\Exceptions\NotImplemented as AclNotImplemented;
use Espo\ORM\EntityManager;
use Espo\Services\Stream as Service;
use Espo\Services\Notification as NotificationService;
use Espo\Tools\Notification\Service as NotificationService;
/**
* Handles auto-follow.