diff --git a/application/Espo/Acl/ActionHistoryRecord.php b/application/Espo/Acl/ActionHistoryRecord.php index 5a00e22113..9b8d0a73c6 100644 --- a/application/Espo/Acl/ActionHistoryRecord.php +++ b/application/Espo/Acl/ActionHistoryRecord.php @@ -39,6 +39,6 @@ class ActionHistoryRecord extends Acl { public function checkIsOwner(EntityUser $user, Entity $entity) { - return $entity->get('userId') === $user->id; + return $entity->get('userId') === $user->getId(); } } diff --git a/application/Espo/Acl/Attachment.php b/application/Espo/Acl/Attachment.php index e40e8d27e0..23cd4eed37 100644 --- a/application/Espo/Acl/Attachment.php +++ b/application/Espo/Acl/Attachment.php @@ -29,15 +29,23 @@ namespace Espo\Acl; -use Espo\Entities\User as EntityUser; +use Espo\Entities\{ + User as EntityUser, + Note, +}; use Espo\ORM\Entity; -use Espo\Core\Acl\Acl; +use Espo\Core\{ + Acl\Acl, + Acl\ScopeData, + Acl\Table, + Acl\EntityReadAcl, +}; -class Attachment extends Acl +class Attachment extends Acl implements EntityReadAcl { - public function checkEntityRead(EntityUser $user, Entity $entity, $data) + public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($user->isAdmin()) { return true; @@ -62,12 +70,8 @@ class Attachment extends Acl $parent = $this->entityManager->getEntity($entity->get('relatedType'), $entity->get('relatedId')); } - if (!$hasParent) { - return true; - } - - if (!$parent) { - if ($this->checkEntity($user, $entity, $data, 'read')) { + if (!$parent || !$hasParent) { + if ($this->checkEntity($user, $entity, $data, Table::ACTION_READ)) { return true; } @@ -75,31 +79,67 @@ class Attachment extends Acl } if ($parent->getEntityType() === 'Note') { - if (!$parent->get('parentId') || !$parent->get('parentType')) { - return true; - } + $result = $this->checkEntityReadNoteParent($user, $parent); - $parentOfParent = $this->entityManager - ->getEntity($parent->get('parentType'), $parent->get('parentId')); - - if ($parentOfParent && $this->aclManager->checkEntity($user, $parentOfParent)) { - return true; + if ($result !== null) { + return $result; } } else if ($this->aclManager->checkEntity($user, $parent)) { return true; } - if ($this->checkEntity($user, $entity, $data, 'read')) { + if ($this->checkEntity($user, $entity, $data, Table::ACTION_READ)) { return true; } return false; } + protected function checkEntityReadNoteParent(EntityUser $user, Note $note): ?bool + { + if ($note->getTargetType() === Note::TARGET_TEAMS) { + $intersect = array_intersect( + $note->getLinkMultipleIdList('teams'), + $user->getLinkMultipleIdList('teams') + ); + + if (count($intersect)) { + return true; + } + + return null; + } + + if ($note->getTargetType() === Note::TARGET_USERS) { + $isRelated = $this->entityManager + ->getRDBRepository('Note') + ->getRelation($note, 'users') + ->isRelated($user); + + if ($isRelated) { + return true; + } + + return null; + } + + if (!$note->getParentId() || !$note->getParentType()) { + return null; + } + + $parent = $this->entityManager->getEntity($note->getParentType(), $note->getParentId()); + + if ($parent && $this->aclManager->checkEntity($user, $parent)) { + return true; + } + + return null; + } + public function checkIsOwner(EntityUser $user, Entity $entity) { - if ($user->getId() === $entity->get('createdById')) { + if ($user->getId() === $entity->get(self::ATTR_CREATED_BY_ID)) { return true; } diff --git a/application/Espo/Acl/Email.php b/application/Espo/Acl/Email.php index 0e44b579c3..9b67d1e7a9 100644 --- a/application/Espo/Acl/Email.php +++ b/application/Espo/Acl/Email.php @@ -33,23 +33,28 @@ use Espo\Entities\User as EntityUser; use Espo\ORM\Entity; -use Espo\Core\Acl\Acl; +use Espo\Core\{ + Acl\Acl, + Acl\ScopeData, + Acl\Table, + Acl\EntityReadAcl, +}; -class Email extends Acl +class Email extends Acl implements EntityReadAcl { protected $ownerUserIdAttribute = 'usersIds'; - public function checkEntityRead(EntityUser $user, Entity $entity, $data) + public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($this->checkEntity($user, $entity, $data, 'read')) { return true; } - if (!$data) { + if (!$data->isFalse()) { return false; } - if ($data->read === false || $data->read === 'no') { + if ($data->getRead() === Table::LEVEL_NO) { return false; } @@ -83,13 +88,13 @@ class Email extends Acl return false; } - public function checkEntityDelete(EntityUser $user, Entity $entity, $data) + public function checkEntityDelete(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($user->isAdmin()) { return true; } - if (!$data) { + if (!$data->isFalse()) { return false; } @@ -114,11 +119,11 @@ class Email extends Acl return false; } - if ($this->checkEntity($user, $entity, $data, 'delete')) { + if ($this->checkEntity($user, $entity, $data, Table::ACTION_DELETE)) { return true; } - if ($data->edit === 'no' && $data->create === 'no') { + if ($data->getEdit() === Table::LEVEL_NO && $data->getCreate() === Table::LEVEL_NO) { return false; } diff --git a/application/Espo/Acl/Import.php b/application/Espo/Acl/Import.php index 2e87cda21f..32da111770 100644 --- a/application/Espo/Acl/Import.php +++ b/application/Espo/Acl/Import.php @@ -33,30 +33,34 @@ use Espo\Entities\User as EntityUser; use Espo\ORM\Entity; -use Espo\Core\Acl\Acl; +use Espo\Core\{ + Acl\Acl, + Acl\ScopeData, + Acl\EntityReadAcl, +}; -class Import extends Acl +class Import extends Acl implements EntityReadAcl { - public function checkEntityRead(EntityUser $user, Entity $entity, $data) + public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($user->isAdmin()) { return true; } - if ($user->id === $entity->get('createdById')) { + if ($user->getId() === $entity->get('createdById')) { return true; } return false; } - public function checkEntityDelete(EntityUser $user, Entity $entity, $data) + public function checkEntityDelete(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($user->isAdmin()) { return true; } - if ($user->id === $entity->get('createdById')) { + if ($user->getId() === $entity->get('createdById')) { return true; } diff --git a/application/Espo/Acl/Note.php b/application/Espo/Acl/Note.php index 4dfe2813d7..49b9a55b89 100644 --- a/application/Espo/Acl/Note.php +++ b/application/Espo/Acl/Note.php @@ -33,12 +33,17 @@ use Espo\Entities\User as EntityUser; use Espo\ORM\Entity; -use Espo\Core\Acl\Acl; +use Espo\Core\{ + Acl\Acl, + Acl\ScopeData, + Acl\Table, + Acl\EntityEditAcl, +}; use Exception; use DateTime; -class Note extends Acl +class Note extends Acl implements EntityEditAcl { protected $deleteThresholdPeriod = '1 month'; @@ -53,7 +58,7 @@ class Note extends Acl return false; } - public function checkEntityCreate(EntityUser $user, Entity $entity, $data) + public function checkEntityCreate(EntityUser $user, Entity $entity, ScopeData $data): bool { if (!$entity->get('parentId') || !$entity->get('parentType')) { return true; @@ -68,13 +73,13 @@ class Note extends Acl return false; } - public function checkEntityEdit(EntityUser $user, Entity $entity, $data) + public function checkEntityEdit(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($user->isAdmin()) { return true; } - if (!$this->checkEntity($user, $entity, $data, 'edit')) { + if (!$this->checkEntity($user, $entity, $data, Table::ACTION_EDIT)) { return false; } @@ -107,13 +112,13 @@ class Note extends Acl return true; } - public function checkEntityDelete(EntityUser $user, Entity $entity, $data) + public function checkEntityDelete(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($user->isAdmin()) { return true; } - if (!$this->checkEntity($user, $entity, $data, 'delete')) { + if (!$this->checkEntity($user, $entity, $data, Table::ACTION_DELETE)) { return false; } diff --git a/application/Espo/Acl/Notification.php b/application/Espo/Acl/Notification.php index 59d41ad682..347870173f 100644 --- a/application/Espo/Acl/Notification.php +++ b/application/Espo/Acl/Notification.php @@ -39,7 +39,7 @@ class Notification extends Acl { public function checkIsOwner(EntityUser $user, Entity $entity) { - if ($user->id === $entity->get('userId')) { + if ($user->getId() === $entity->get('userId')) { return true; } diff --git a/application/Espo/Acl/ScheduledJob.php b/application/Espo/Acl/ScheduledJob.php index 9d4b10766a..9bc492f1ec 100644 --- a/application/Espo/Acl/ScheduledJob.php +++ b/application/Espo/Acl/ScheduledJob.php @@ -33,43 +33,50 @@ use Espo\Entities\User as EntityUser; use Espo\ORM\Entity; -use Espo\Core\Acl\Acl; +use Espo\Core\{ + Acl\Acl, + Acl\ScopeData, + Acl\Table, + Acl\EntityCreateAcl, + Acl\EntityReadAcl, + Acl\EntityEditAcl, +}; -class ScheduledJob extends Acl +class ScheduledJob extends Acl implements EntityCreateAcl, EntityReadAcl, EntityEditAcl { - public function checkEntityRead(EntityUser $user, Entity $entity, $data) + public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($entity->get('isInternal')) { return false; } - return $this->checkEntity($user, $entity, $data, 'read'); + return $this->checkEntity($user, $entity, $data, Table::ACTION_READ); } - public function checkEntityEdit(EntityUser $user, Entity $entity, $data) + public function checkEntityEdit(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($entity->get('isInternal')) { return false; } - return $this->checkEntity($user, $entity, $data, 'edit'); + return $this->checkEntity($user, $entity, $data, Table::ACTION_EDIT); } - public function checkEntityDelete(EntityUser $user, Entity $entity, $data) + public function checkEntityDelete(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($entity->get('isInternal')) { return false; } - return $this->checkEntity($user, $entity, $data, 'delete'); + return $this->checkEntity($user, $entity, $data, Table::ACTION_DELETE); } - public function checkEntityCreate(EntityUser $user, Entity $entity, $data) + public function checkEntityCreate(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($entity->get('isInternal')) { return false; } - return $this->checkEntity($user, $entity, $data, 'create'); + return $this->checkEntity($user, $entity, $data, Table::ACTION_CREATE); } } diff --git a/application/Espo/Acl/Team.php b/application/Espo/Acl/Team.php index c79b06ae56..3448c5e457 100644 --- a/application/Espo/Acl/Team.php +++ b/application/Espo/Acl/Team.php @@ -41,6 +41,6 @@ class Team extends Acl { $userTeamIdList = $user->getLinkMultipleIdList('teams'); - return in_array($entity->id, $userTeamIdList); + return in_array($entity->getId(), $userTeamIdList); } } diff --git a/application/Espo/Acl/User.php b/application/Espo/Acl/User.php index d42781a91f..7bde9ac963 100644 --- a/application/Espo/Acl/User.php +++ b/application/Espo/Acl/User.php @@ -33,27 +33,34 @@ use Espo\ORM\Entity; use Espo\Entities\User as EntityUser; -use Espo\Core\Acl\Acl; +use Espo\Core\{ + Acl\Acl, + Acl\ScopeData, + Acl\Table, + Acl\EntityCreateAcl, + Acl\EntityReadAcl, + Acl\EntityEditAcl, +}; -class User extends Acl +class User extends Acl implements EntityCreateAcl, EntityReadAcl, EntityEditAcl { public function checkIsOwner(EntityUser $user, Entity $entity) { - return $user->id === $entity->id; + return $user->getId() === $entity->getId(); } - public function checkEntityRead(EntityUser $user, Entity $entity, $data) + public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { if (!$user->isAdmin() && $entity->isPortal()) { - if ($this->getAclManager()->get($user, 'portalPermission') === 'yes') { + if ($this->getAclManager()->get($user, 'portal') === Table::LEVEL_YES) { return true; } } - return $this->checkEntity($user, $entity, $data, 'read'); + return $this->checkEntity($user, $entity, $data, Table::ACTION_READ); } - public function checkEntityCreate(EntityUser $user, Entity $entity, $data) + public function checkEntityCreate(EntityUser $user, Entity $entity, ScopeData $data): bool { if (!$user->isAdmin()) { return false; @@ -63,12 +70,12 @@ class User extends Acl return false; } - return $this->checkEntity($user, $entity, $data, 'create'); + return $this->checkEntity($user, $entity, $data, Table::ACTION_CREATE); } - public function checkEntityDelete(EntityUser $user, Entity $entity, $data) + public function checkEntityDelete(EntityUser $user, Entity $entity, ScopeData $data): bool { - if ($entity->id === 'system') { + if ($entity->getId() === 'system') { return false; } @@ -87,7 +94,7 @@ class User extends Acl return parent::checkEntityDelete($user, $entity, $data); } - public function checkEntityEdit(EntityUser $user, Entity $entity, $data) + public function checkEntityEdit(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($entity->id === 'system') { return false; @@ -97,7 +104,7 @@ class User extends Acl } if (!$user->isAdmin()) { - if ($user->id !== $entity->id) { + if ($user->getId() !== $entity->getId()) { return false; } } @@ -106,6 +113,6 @@ class User extends Acl return false; } - return $this->checkEntity($user, $entity, $data, 'edit'); + return $this->checkEntity($user, $entity, $data, Table::ACTION_EDIT); } } diff --git a/application/Espo/Acl/Webhook.php b/application/Espo/Acl/Webhook.php index 4816d3639f..910f27678e 100644 --- a/application/Espo/Acl/Webhook.php +++ b/application/Espo/Acl/Webhook.php @@ -33,22 +33,28 @@ use Espo\Entities\User as EntityUser; use Espo\ORM\Entity; -use Espo\Core\Acl\Acl; +use Espo\Core\{ + Acl\Acl, + Acl\ScopeData, + Acl\EntityCreateAcl, + Acl\EntityReadAcl, + Acl\EntityEditAcl, +}; -class Webhook extends Acl +class Webhook extends Acl implements EntityCreateAcl, EntityReadAcl, EntityEditAcl { public function checkIsOwner(EntityUser $user, Entity $entity) { return $user->id === $entity->get('userId') && $user->isApi(); } - public function checkEntityCreate(EntityUser $user, Entity $entity, $data) + public function checkEntityCreate(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($user->isAdmin()) { return true; } - if (!$data) { + if ($data->isFalse()) { return false; } @@ -59,13 +65,13 @@ class Webhook extends Acl return false; } - public function checkEntityRead(EntityUser $user, Entity $entity, $data) + public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($user->isAdmin()) { return true; } - if (!$data) { + if ($data->isFalse()) { return false; } @@ -76,13 +82,13 @@ class Webhook extends Acl return false; } - public function checkEntityDelete(EntityUser $user, Entity $entity, $data) + public function checkEntityDelete(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($user->isAdmin()) { return true; } - if (!$data) { + if ($data->isFalse()) { return false; } @@ -93,13 +99,13 @@ class Webhook extends Acl return false; } - public function checkEntityEdit(EntityUser $user, Entity $entity, $data) + public function checkEntityEdit(EntityUser $user, Entity $entity, ScopeData $data): bool { if ($user->isAdmin()) { return true; } - if (!$data) { + if ($data->isFalse()) { return false; } diff --git a/application/Espo/AclPortal/Attachment.php b/application/Espo/AclPortal/Attachment.php index 75cb2f8a68..5f71daa5d1 100644 --- a/application/Espo/AclPortal/Attachment.php +++ b/application/Espo/AclPortal/Attachment.php @@ -29,65 +29,113 @@ namespace Espo\AclPortal; -use \Espo\Entities\User as EntityUser; -use \Espo\ORM\Entity; +use Espo\Entities\User as EntityUser; +use Espo\Entities\Note; -class Attachment extends \Espo\Core\AclPortal\Base +use Espo\ORM\Entity; + +use Espo\Core\{ + Acl\ScopeData, + Acl\Table, + Acl\EntityReadAcl, + AclPortal\Acl as Acl, +}; + +class Attachment extends Acl implements EntityReadAcl { - public function checkEntityRead(EntityUser $user, Entity $entity, $data) + public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { - if ($user->isAdmin()) { - return true; - } - if ($entity->get('parentType') === 'Settings') { return true; } $parent = null; $hasParent = false; + if ($entity->get('parentId') && $entity->get('parentType')) { $hasParent = true; - $parent = $this->getEntityManager()->getEntity($entity->get('parentType'), $entity->get('parentId')); - } else if ($entity->get('relatedId') && $entity->get('relatedType')) { + + $parent = $this->entityManager->getEntity($entity->get('parentType'), $entity->get('parentId')); + } + else if ($entity->get('relatedId') && $entity->get('relatedType')) { $hasParent = true; - $parent = $this->getEntityManager()->getEntity($entity->get('relatedType'), $entity->get('relatedId')); + + $parent = $this->entityManager->getEntity($entity->get('relatedType'), $entity->get('relatedId')); } - if ($hasParent) { - if ($parent) { - if ($parent->getEntityType() === 'Note') { - if ($parent->get('parentId') && $parent->get('parentType')) { - $parentOfParent = $this->getEntityManager()->getEntity($parent->get('parentType'), $parent->get('parentId')); - if ($parentOfParent && $this->getAclManager()->checkEntity($user, $parentOfParent)) { - return true; - } - } else { - return true; - } - } else { - if ($this->getAclManager()->checkEntity($user, $parent)) { - return true; - } - } + if (!$hasParent) { + return false; + } + + if ($parent->getEntityType() === 'Note') { + $result = $this->checkEntityReadNoteParent($user, $parent); + + if ($result !== null) { + return $result; } - } else { + } + else if ($this->aclManager->checkEntity($user, $parent)) { return true; } - if ($this->checkEntity($user, $entity, $data, 'read')) { + if ($this->checkEntity($user, $entity, $data, Table::ACTION_READ)) { return true; } return false; } + protected function checkEntityReadNoteParent(EntityUser $user, Note $note): ?bool + { + if ($note->isInternal()) { + return false; + } + + if ($note->getTargetType() === Note::TARGET_PORTALS) { + $intersect = array_intersect( + $note->getLinkMultipleIdList('portals'), + $user->getLinkMultipleIdList('portals') + ); + + if (count($intersect)) { + return true; + } + + return false; + } + + if ($note->getTargetType() === Note::TARGET_USERS) { + $isRelated = $this->entityManager + ->getRDBRepository('Note') + ->getRelation($note, 'users') + ->isRelated($user); + + if ($isRelated) { + return true; + } + + return false; + } + + if (!$note->getParentId() || !$note->getParentType()) { + return null; + } + + $parent = $this->entityManager->getEntity($note->getParentType(), $note->getParentId()); + + if ($parent && $this->aclManager->checkEntity($user, $parent)) { + return true; + } + + return null; + } + public function checkIsOwner(EntityUser $user, Entity $entity) { - if ($user->id === $entity->get('createdById')) { + if ($user->getId() === $entity->get('createdById')) { return true; } + return false; } } - diff --git a/application/Espo/AclPortal/Email.php b/application/Espo/AclPortal/Email.php index d50481d7e1..b5a16c6ce2 100644 --- a/application/Espo/AclPortal/Email.php +++ b/application/Espo/AclPortal/Email.php @@ -29,43 +29,54 @@ namespace Espo\AclPortal; -use \Espo\Entities\User as EntityUser; -use \Espo\ORM\Entity; +use Espo\Entities\User as EntityUser; -class Email extends \Espo\Core\AclPortal\Base +use Espo\ORM\Entity; + +use Espo\Core\{ + Acl\ScopeData, + Acl\Table, + Acl\EntityReadAcl, + AclPortal\Acl as Acl, +}; + +class Email extends Acl implements EntityReadAcl { protected $ownerUserIdAttribute = 'usersIds'; - public function checkEntityRead(EntityUser $user, Entity $entity, $data) + public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { - if ($this->checkEntity($user, $entity, $data, 'read')) { + if ($this->checkEntity($user, $entity, $data, Table::ACTION_READ)) { return true; } - if ($data === false) { + if ($data->isFalse()) { return false; } - if (is_object($data)) { - if ($data->read === false || $data->read === 'no') { - return false; - } + + if ($data->getRead() === Table::LEVEL_NO) { + return false; } if (!$entity->has('usersIds')) { $entity->loadLinkMultipleField('users'); } + $userIdList = $entity->get('usersIds'); - if (is_array($userIdList) && in_array($user->id, $userIdList)) { + + if (is_array($userIdList) && in_array($user->getId(), $userIdList)) { return true; } + return false; } public function checkIsOwner(EntityUser $user, Entity $entity) { - if ($user->id === $entity->get('createdById')) { + if ($user->getId() === $entity->get('createdById')) { return true; } + return false; } } diff --git a/application/Espo/AclPortal/EmailAddress.php b/application/Espo/AclPortal/EmailAddress.php index f5e400e8d5..909d94e449 100644 --- a/application/Espo/AclPortal/EmailAddress.php +++ b/application/Espo/AclPortal/EmailAddress.php @@ -29,14 +29,19 @@ namespace Espo\AclPortal; -use \Espo\Entities\User as EntityUser; -use \Espo\ORM\Entity; +use Espo\Entities\User as EntityUser; +use Espo\ORM\Entity; -class EmailAddress extends \Espo\Core\AclPortal\Base +use Espo\Core\{ + Acl\Table, + AclPortal\Acl as Acl, +}; + +class EmailAddress extends Acl { public function checkEditInEntity(EntityUser $user, Entity $entity, Entity $excludeEntity) : bool { - $id = $entity->id; + $id = $entity->getId(); $isFobidden = false; @@ -46,13 +51,14 @@ class EmailAddress extends \Espo\Core\AclPortal\Base $entityWithSameAddressList = $repository->getEntityListByAddressId($id, $excludeEntity); foreach ($entityWithSameAddressList as $e) { - if (!$this->getAclManager()->check($user, $e, 'edit')) { + if (!$this->getAclManager()->check($user, $e, Table::ACTION_EDIT)) { $isFobidden = true; break; } } } + return !$isFobidden; } } diff --git a/application/Espo/AclPortal/Note.php b/application/Espo/AclPortal/Note.php index ab3e29335f..cda6557d3b 100644 --- a/application/Espo/AclPortal/Note.php +++ b/application/Espo/AclPortal/Note.php @@ -29,22 +29,32 @@ namespace Espo\AclPortal; -use \Espo\Entities\User as EntityUser; -use \Espo\ORM\Entity; +use Espo\Entities\User as EntityUser; +use Espo\ORM\Entity; -class Note extends \Espo\Core\AclPortal\Base +use Espo\Core\{ + Acl\ScopeData, + Acl\Table, + Acl\EntityCreateAcl, + AclPortal\Acl as Acl, +}; + +class Note extends Acl implements EntityCreateAcl { public function checkIsOwner(EntityUser $user, Entity $entity) { if ($entity->get('type') === 'Post' && $user->id === $entity->get('createdById')) { return true; } + return false; } - public function checkEntityCreate(EntityUser $user, Entity $entity, $data) + public function checkEntityCreate(EntityUser $user, Entity $entity, ScopeData $data): bool { - if ($entity->get('type') !== 'Post') return false; + if ($entity->get('type') !== 'Post') { + return false; + } if ($entity->get('type') === 'Post' && $entity->get('targetType')) { return false; @@ -55,8 +65,9 @@ class Note extends \Espo\Core\AclPortal\Base } $parent = $this->getEntityManager()->getEntity($entity->get('parentType'), $entity->get('parentId')); + if ($parent) { - if ($this->getAclManager()->checkEntity($user, $parent, 'stream')) { + if ($this->getAclManager()->checkEntity($user, $parent, Table::ACTION_STREAM)) { return true; } } diff --git a/application/Espo/AclPortal/Notification.php b/application/Espo/AclPortal/Notification.php index b51ab49332..fd4ce53ed2 100644 --- a/application/Espo/AclPortal/Notification.php +++ b/application/Espo/AclPortal/Notification.php @@ -29,17 +29,21 @@ namespace Espo\AclPortal; -use \Espo\Entities\User as EntityUser; -use \Espo\ORM\Entity; +use Espo\Entities\User as EntityUser; +use Espo\ORM\Entity; -class Notification extends \Espo\Core\AclPortal\Base +use Espo\Core\{ + AclPortal\Acl as Acl, +}; + +class Notification extends Acl { public function checkIsOwner(EntityUser $user, Entity $entity) { - if ($user->id === $entity->get('userId')) { + if ($user->getId() === $entity->get('userId')) { return true; } + return false; } } - diff --git a/application/Espo/AclPortal/PhoneNumber.php b/application/Espo/AclPortal/PhoneNumber.php index 3b7639531c..8e8ba39763 100644 --- a/application/Espo/AclPortal/PhoneNumber.php +++ b/application/Espo/AclPortal/PhoneNumber.php @@ -29,14 +29,19 @@ namespace Espo\AclPortal; -use \Espo\Entities\User as EntityUser; -use \Espo\ORM\Entity; +use Espo\Entities\User as EntityUser; +use Espo\ORM\Entity; -class PhoneNumber extends \Espo\Core\AclPortal\Base +use Espo\Core\{ + Acl\Table, + AclPortal\Acl as Acl, +}; + +class PhoneNumber extends Acl { public function checkEditInEntity(EntityUser $user, Entity $entity, Entity $excludeEntity) : bool { - $id = $entity->id; + $id = $entity->getId(); $isFobidden = false; @@ -46,7 +51,7 @@ class PhoneNumber extends \Espo\Core\AclPortal\Base $entityWithSameNumberList = $repository->getEntityListByPhoneNumberId($id, $excludeEntity); foreach ($entityWithSameNumberList as $e) { - if (!$this->getAclManager()->check($user, $e, 'edit')) { + if (!$this->getAclManager()->check($user, $e, Table::ACTION_EDIT)) { $isFobidden = true; break; diff --git a/application/Espo/AclPortal/User.php b/application/Espo/AclPortal/User.php index 2e6beb2d43..5990fbf8c9 100644 --- a/application/Espo/AclPortal/User.php +++ b/application/Espo/AclPortal/User.php @@ -29,13 +29,17 @@ namespace Espo\AclPortal; -use \Espo\ORM\Entity; +use Espo\ORM\Entity; +use Espo\Entities\User as UserEntity; -class User extends \Espo\Core\AclPortal\Base +use Espo\Core\{ + AclPortal\Acl as Acl, +}; + +class User extends Acl { - public function checkIsOwner(\Espo\Entities\User $user, Entity $entity) + public function checkIsOwner(UserEntity $user, Entity $entity) { - return $user->id === $entity->id; + return $user->getId() === $entity->getId(); } } - diff --git a/application/Espo/Core/Acl.php b/application/Espo/Core/Acl.php index 350a682bb3..2034ad0300 100644 --- a/application/Espo/Core/Acl.php +++ b/application/Espo/Core/Acl.php @@ -53,7 +53,7 @@ class Acl $this->user = $user; } - public function getMap() : StdClass + public function getMap(): StdClass { return $this->aclManager->getMap($this->user); } @@ -61,7 +61,7 @@ class Acl /** * Get an access level for a specific scope and action. */ - public function getLevel(string $scope, string $action) : string + public function getLevel(string $scope, string $action): string { return $this->aclManager->getLevel($this->user, $scope, $action); } @@ -69,7 +69,7 @@ class Acl /** * Get a permission. E.g. 'assignment' permission. */ - public function get(string $permission) : ?string + public function get(string $permission): ?string { return $this->aclManager->get($this->user, $permission); } @@ -77,7 +77,7 @@ class Acl /** * Whether there's no 'read' access for a specific scope. */ - public function checkReadNo(string $scope) : bool + public function checkReadNo(string $scope): bool { return $this->aclManager->checkReadNo($this->user, $scope); } @@ -85,7 +85,7 @@ class Acl /** * Whether 'read' access is set to 'team' for a specific scope. */ - public function checkReadOnlyTeam(string $scope) : bool + public function checkReadOnlyTeam(string $scope): bool { return $this->aclManager->checkReadOnlyTeam($this->user, $scope); } @@ -93,7 +93,7 @@ class Acl /** * Whether 'read' access is set to 'own' for a specific scope. */ - public function checkReadOnlyOwn(string $scope) : bool + public function checkReadOnlyOwn(string $scope): bool { return $this->aclManager->checkReadOnlyOwn($this->user, $scope); } @@ -101,7 +101,7 @@ class Acl /** * Check a scope or entity. If $action is omitted, it will check whether a scope level is set to 'enabled'. */ - public function check($subject, ?string $action = null) : bool + public function check($subject, ?string $action = null): bool { return $this->aclManager->check($this->user, $subject, $action); } @@ -109,23 +109,63 @@ class Acl /** * Check access to scope. If $action is omitted, it will check whether a scope level is set to 'enabled'. */ - public function checkScope(string $scope, ?string $action = null) : bool + public function checkScope(string $scope, ?string $action = null): bool { return $this->aclManager->checkScope($this->user, $scope, $action); } /** - * Check access to a specific entity (record). + * Check access to a specific entity. */ - public function checkEntity(Entity $entity, string $action = Table::ACTION_READ) : bool + public function checkEntity(Entity $entity, string $action = Table::ACTION_READ): bool { return $this->aclManager->checkEntity($this->user, $entity, $action); } + /** + * Check 'read' access to a specific entity. + */ + public function checkEntityRead(Entity $entity): bool + { + return $this->checkEntity($entity, Table::ACTION_READ); + } + + /** + * Check 'create' access to a specific entity. + */ + public function checkEntityCreate(Entity $entity): bool + { + return $this->checkEntity($entity, Table::ACTION_CREATE); + } + + /** + * Check 'edit' access to a specific entity. + */ + public function checkEntityEdit(Entity $entity): bool + { + return $this->checkEntity($entity, Table::ACTION_EDIT); + } + + /** + * Check 'delete' access to a specific entity. + */ + public function checkEntityDelete(Entity $entity): bool + { + return $this->checkEntity($entity, Table::ACTION_DELETE); + } + + /** + * Check 'stream' access to a specific entity. + */ + public function checkEntityStream(Entity $entity): bool + { + return $this->checkEntity($entity, Table::ACTION_STREAM); + } + /** * @deprecated */ - public function checkUser(string $permission, User $entity) : bool + public function checkUser(string $permission, User $entity): bool { return $this->aclManager->checkUser($this->user, $permission, $entity); } @@ -133,7 +173,7 @@ class Acl /** * Whether a user is owned of an entity (record). Usually 'assignedUser' field is used for checking. */ - public function checkIsOwner(Entity $entity) : bool + public function checkIsOwner(Entity $entity): bool { return $this->aclManager->checkIsOwner($this->user, $entity); } @@ -141,7 +181,7 @@ class Acl /** * Whether a user team list overlaps with teams set in an entity. */ - public function checkInTeam(Entity $entity) : bool + public function checkInTeam(Entity $entity): bool { return $this->aclManager->checkInTeam($this->user, $entity); } @@ -150,8 +190,10 @@ class Acl * Get attributes forbidden for a user. */ public function getScopeForbiddenAttributeList( - string $scope, string $action = Table::ACTION_READ, string $thresholdLevel = Table::LEVEL_NO - ) : array { + string $scope, + string $action = Table::ACTION_READ, + string $thresholdLevel = Table::LEVEL_NO + ): array { return $this->aclManager ->getScopeForbiddenAttributeList($this->user, $scope, $action, $thresholdLevel); @@ -161,8 +203,10 @@ class Acl * Get fields forbidden for a user. */ public function getScopeForbiddenFieldList( - string $scope, string $action = Table::ACTION_READ, string $thresholdLevel = Table::LEVEL_NO - ) : array { + string $scope, + string $action = Table::ACTION_READ, + string $thresholdLevel = Table::LEVEL_NO + ): array { return $this->aclManager ->getScopeForbiddenFieldList($this->user, $scope, $action, $thresholdLevel); @@ -172,8 +216,10 @@ class Acl * Get links forbidden for a user. */ public function getScopeForbiddenLinkList( - string $scope, string $action = Table::ACTION_READ, string $thresholdLevel = Table::LEVEL_NO - ) : array { + string $scope, + string $action = Table::ACTION_READ, + string $thresholdLevel = Table::LEVEL_NO + ): array { return $this->aclManager->getScopeForbiddenLinkList($this->user, $scope, $action, $thresholdLevel); } @@ -183,7 +229,7 @@ class Acl * * @param User|string $target User entity or user ID. */ - public function checkUserPermission($target, string $permissionType = 'user') : bool + public function checkUserPermission($target, string $permissionType = 'user'): bool { return $this->aclManager->checkUserPermission($this->user, $target, $permissionType); } @@ -193,22 +239,22 @@ class Acl * * @param User|string $target User entity or user ID. */ - public function checkAssignmentPermission($target) : bool + public function checkAssignmentPermission($target): bool { return $this->aclManager->checkAssignmentPermission($this->user, $target); } - public function getScopeRestrictedFieldList(string $scope, $type) : array + public function getScopeRestrictedFieldList(string $scope, $type): array { return $this->aclManager->getScopeRestrictedFieldList($scope, $type); } - public function getScopeRestrictedAttributeList(string $scope, $type) : array + public function getScopeRestrictedAttributeList(string $scope, $type): array { return $this->aclManager->getScopeRestrictedAttributeList($scope, $type); } - public function getScopeRestrictedLinkList(string $scope, $type) : array + public function getScopeRestrictedLinkList(string $scope, $type): array { return $this->aclManager->getScopeRestrictedLinkList($scope, $type); } diff --git a/application/Espo/Core/Acl/Acl.php b/application/Espo/Core/Acl/Acl.php index 02fe861af9..5b8186a986 100644 --- a/application/Espo/Core/Acl/Acl.php +++ b/application/Espo/Core/Acl/Acl.php @@ -43,99 +43,83 @@ use Espo\Core\{ /** * An implementation for access checking for entities. Can be overridden in `Acl` namespace. */ -class Acl implements ScopeAcl, EntityAcl +class Acl implements ScopeAcl, EntityAcl, EntityDeleteAcl { + protected const ATTR_CREATED_BY_ID = 'createdById'; + + protected const ATTR_ASSIGNED_USER_ID = 'assignedUserId'; + + protected const ATTR_ASSIGNED_USERS_IDS = 'assignedUsersIds'; + + protected const ATTR_ASSIGNED_TEAMS_IDS = 'teamsIds'; + + protected const FIELD_TEAMS = 'teams'; + + protected const FIELD_ASSIGNED_USERS = 'assignedUsers'; + protected $scope; protected $ownerUserIdAttribute = null; protected $allowDeleteCreatedThresholdPeriod = '24 hours'; - protected $entityManager; + protected $checkIsOwnerentityManager; protected $aclManager; protected $config; - public function __construct(string $scope, EntityManager $entityManager, AclManager $aclManager, Config $config) - { - $this->scope = $scope; - + public function __construct( + EntityManager $entityManager, + AclManager $aclManager, + Config $config, + string $scope = null + ) { $this->entityManager = $entityManager; $this->aclManager = $aclManager; $this->config = $config; + + $this->scope = $scope; } - protected function getConfig() + public function checkEntity(User $user, Entity $entity, ScopeData $data, string $action = Table::ACTION_READ): bool { - return $this->config; + return $this->checkScopeInternal($user, $data, $action, $entity); } - protected function getEntityManager() + public function checkScope(User $user, ScopeData $data, ?string $action = null) : bool { - return $this->entityManager; + return $this->checkScopeInternal($user, $data, $action); } - protected function getAclManager() + public function checkReadOnlyTeam(User $user, ScopeData $data) : bool { - return $this->aclManager; + return $data->getRead() === Table::LEVEL_TEAM; } - public function checkReadOnlyTeam(User $user, $data) + public function checkReadNo(User $user, ScopeData $data) : bool { - if (empty($data) || !is_object($data) || !isset($data->read)) { + return $data->getRead() === Table::LEVEL_NO; + } + + public function checkReadOnlyOwn(User $user, ScopeData $data) : bool + { + return $data->getRead() === Table::LEVEL_OWN; + } + + protected function checkScopeInternal( + User $user, + ScopeData $data, + ?string $action = null, + ?Entity $entity = null, + array $entityAccessData = [] + ) : bool { + + if ($data->isFalse()) { return false; } - return $data->read === Table::LEVEL_TEAM; - } - - public function checkReadNo(User $user, $data) - { - if (empty($data) || !is_object($data) || !isset($data->read)) { - return false; - } - - return $data->read === Table::LEVEL_NO; - } - - public function checkReadOnlyOwn(User $user, $data) - { - if (empty($data) || !is_object($data) || !isset($data->read)) { - return false; - } - - return $data->read === Table::LEVEL_OWN; - } - - public function checkEntity(User $user, Entity $entity, $data, $action) - { - if ($user->isAdmin()) { - return true; - } - - return $this->checkScope($user, $data, $action, $entity); - } - - public function checkScope(User $user, $data, $action = null, Entity $entity = null, $entityAccessData = []) - { - if ($user->isAdmin()) { - return true; - } - - if (is_null($data)) { - return false; - } - - if ($data === false) { - return false; - } - - if ($data === true) { - return true; - } - - if (is_string($data)) { + if ($data->isTrue()) { return true; } @@ -152,20 +136,20 @@ class Acl implements ScopeAcl, EntityAcl } if (is_null($action)) { - return true; - } + if ($data->hasNotNo()) { + return true; + } - if (!isset($data->$action)) { return false; } - $value = $data->$action; + $value = $data->get($action); - if ($value === Table::LEVEL_ALL || $value === Table::LEVEL_YES || $value === true) { + if ($value === Table::LEVEL_ALL || $value === Table::LEVEL_YES) { return true; } - if (!$value || $value === Table::LEVEL_NO) { + if ($value === Table::LEVEL_NO) { return false; } @@ -197,25 +181,30 @@ class Acl implements ScopeAcl, EntityAcl return false; } + /** + * @return bool + */ public function checkIsOwner(User $user, Entity $entity) { - if ($entity->hasAttribute('assignedUserId')) { - if ($entity->has('assignedUserId')) { - if ($user->id === $entity->get('assignedUserId')) { - return true; - } + if ($entity->hasAttribute(self::ATTR_ASSIGNED_USER_ID)) { + if ( + $entity->has(self::ATTR_ASSIGNED_USER_ID) && + $user->getId() === $entity->get(self::ATTR_ASSIGNED_USER_ID) + ) { + return true; } } - else if ($entity->hasAttribute('createdById')) { - if ($entity->has('createdById')) { - if ($user->id === $entity->get('createdById')) { - return true; - } + else if ($entity->hasAttribute(self::ATTR_CREATED_BY_ID)) { + if ( + $entity->has(self::ATTR_CREATED_BY_ID) && + $user->getId() === $entity->get(self::ATTR_CREATED_BY_ID) + ) { + return true; } } - if ($entity->hasLinkMultipleField('assignedUsers')) { - if ($entity->hasLinkMultipleId('assignedUsers', $user->id)) { + if ($entity->hasLinkMultipleField(self::FIELD_ASSIGNED_USERS)) { + if ($entity->hasLinkMultipleId(self::FIELD_ASSIGNED_USERS, $user->getId())) { return true; } } @@ -223,15 +212,18 @@ class Acl implements ScopeAcl, EntityAcl return false; } + /** + * @return bool + */ public function checkInTeam(User $user, Entity $entity) { - $userTeamIdList = $user->getLinkMultipleIdList('teams'); + $userTeamIdList = $user->getLinkMultipleIdList(self::FIELD_TEAMS); - if (!$entity->hasRelation('teams') || !$entity->hasAttribute('teamsIds')) { + if (!$entity->hasRelation(self::FIELD_TEAMS) || !$entity->hasAttribute(self::ATTR_ASSIGNED_TEAMS_IDS)) { return false; } - $entityTeamIdList = $entity->getLinkMultipleIdList('teams'); + $entityTeamIdList = $entity->getLinkMultipleIdList(self::FIELD_TEAMS); if (empty($entityTeamIdList)) { return false; @@ -246,7 +238,7 @@ class Acl implements ScopeAcl, EntityAcl return false; } - public function checkEntityDelete(User $user, Entity $entity, $data) + public function checkEntityDelete(User $user, Entity $entity, ScopeData $data) : bool { if ($user->isAdmin()) { return true; @@ -256,32 +248,28 @@ class Acl implements ScopeAcl, EntityAcl return true; } - if (!is_object($data)) { - return false; - } - - if ($data->edit === Table::LEVEL_NO && $data->create === Table::LEVEL_NO) { + if ($data->getEdit() === Table::LEVEL_NO && $data->getCreate() === Table::LEVEL_NO) { return false; } if ( !$this->config->get('aclAllowDeleteCreated') || - !$entity->has('createdById') || - !$entity->get('createdById') !== $user->getId() + !$entity->has(self::ATTR_CREATED_BY_ID) || + !$entity->get(self::ATTR_CREATED_BY_ID) !== $user->getId() ) { return false; } $isDeletedAllowed = false; - if (!$entity->has('assignedUserId')) { + if (!$entity->has(self::ATTR_ASSIGNED_USER_ID)) { $isDeletedAllowed = true; } else { - if (!$entity->get('assignedUserId')) { + if (!$entity->get(self::ATTR_ASSIGNED_USER_ID)) { $isDeletedAllowed = true; } - else if ($entity->get('assignedUserId') === $entity->get('createdById')) { + else if ($entity->get(self::ATTR_ASSIGNED_USER_ID) === $entity->get(self::ATTR_CREATED_BY_ID)) { $isDeletedAllowed = true; } } @@ -308,22 +296,48 @@ class Acl implements ScopeAcl, EntityAcl return true; } - public function getOwnerUserIdAttribute(Entity $entity) + public function getOwnerUserIdAttribute(Entity $entity) : ?string { if ($this->ownerUserIdAttribute) { return $this->ownerUserIdAttribute; } - if ($entity->hasLinkMultipleField('assignedUsers')) { - return 'assignedUsersIds'; + if ($entity->hasLinkMultipleField(self::FIELD_ASSIGNED_USERS)) { + return self::ATTR_ASSIGNED_USERS_IDS; } - if ($entity->hasAttribute('assignedUserId')) { - return 'assignedUserId'; + if ($entity->hasAttribute(self::ATTR_ASSIGNED_USER_ID)) { + return self::ATTR_ASSIGNED_USER_ID; } - if ($entity->hasAttribute('createdById')) { - return 'createdById'; + if ($entity->hasAttribute(self::ATTR_CREATED_BY_ID)) { + return self::ATTR_CREATED_BY_ID; } + + return null; + } + + /** + * @deprecated Use `$this->config`. + */ + protected function getConfig() : Config + { + return $this->config; + } + + /** + * @deprecated Use `$this->entityManager`. + */ + protected function getEntityManager() : EntityManager + { + return $this->entityManager; + } + + /** + * @deprecated Use `$this->aclManager`. + */ + protected function getAclManager() : AclManager + { + return $this->aclManager; } } diff --git a/application/Espo/Core/Acl/AclFactory.php b/application/Espo/Core/Acl/AclFactory.php new file mode 100644 index 0000000000..9488ab3424 --- /dev/null +++ b/application/Espo/Core/Acl/AclFactory.php @@ -0,0 +1,63 @@ +classFinder = $classFinder; + $this->injectableFactory = $injectableFactory; + } + + public function create(string $scope) : ScopeAcl + { + $className = $this->classFinder->find('Acl', $scope); + + if (!$className) { + $className = $this->baseClassName; + } + + return $this->injectableFactory->createWith($className, [ + 'scope' => $scope, // todo remove ? + ]); + } +} diff --git a/application/Espo/Core/Acl/Base.php b/application/Espo/Core/Acl/Base.php index 4482ce176d..1c2f97bc8c 100644 --- a/application/Espo/Core/Acl/Base.php +++ b/application/Espo/Core/Acl/Base.php @@ -31,9 +31,6 @@ namespace Espo\Core\Acl; use Espo\Core\Interfaces\Injectable; -use Espo\Entities\User; -use Espo\ORM\Entity; - use Espo\Core\{ ORM\EntityManager, AclManager, @@ -49,10 +46,15 @@ class Base extends Acl implements Injectable protected $injections = []; - public function __construct(string $scope, EntityManager $entityManager, AclManager $aclManager, Config $config) - { + public function __construct( + EntityManager $entityManager, + AclManager $aclManager, + Config $config, + ?string $scope = null + ) { $this->scope = $scope; - parent::__construct($scope, $entityManager, $aclManager, $config); + + parent::__construct($entityManager, $aclManager, $config, $scope); $this->init(); } @@ -62,7 +64,6 @@ class Base extends Acl implements Injectable $this->injections[$name] = $object; } - protected function init() { } diff --git a/application/Espo/Core/Acl/EntityAcl.php b/application/Espo/Core/Acl/EntityAcl.php index 172affb77e..546a1d9e62 100644 --- a/application/Espo/Core/Acl/EntityAcl.php +++ b/application/Espo/Core/Acl/EntityAcl.php @@ -29,11 +29,13 @@ namespace Espo\Core\Acl; +use Espo\ORM\Entity; + +use Espo\Entities\User; -/** - * @todo Add methods. - */ interface EntityAcl { + public function checkEntity(User $user, Entity $entity, ScopeData $data, string $action = Table::ACTION_READ): bool; + public function getOwnerUserIdAttribute(Entity $entity): ?string; } diff --git a/application/Espo/Core/Acl/EntityCreateAcl.php b/application/Espo/Core/Acl/EntityCreateAcl.php new file mode 100644 index 0000000000..ec4316d980 --- /dev/null +++ b/application/Espo/Core/Acl/EntityCreateAcl.php @@ -0,0 +1,39 @@ +raw; } + /** + * Is of boolean type. + */ public function isBoolean() : bool { return $this->isBoolean; } + /** + * Is true. + */ public function isTrue() : bool { if (!$this->isBoolean) { @@ -75,6 +81,9 @@ class ScopeData return $this->raw === true; } + /** + * Is false. + */ public function isFalse() : bool { if (!$this->isBoolean) { @@ -84,31 +93,63 @@ class ScopeData return $this->raw === false; } + /** + * Has any level other than 'no'. + */ + public function hasNotNo() : bool + { + foreach ($this->actionData as $level) { + if ($level !== Table::LEVEL_NO) { + return true; + } + } + + return false; + } + + /** + * Get a level for an action. + */ public function get(string $action) : string { return $this->actionData[$action] ?? Table::LEVEL_NO; } + /** + * Get a 'read' level. + */ public function getRead() : string { return $this->get(Table::ACTION_READ); } + /** + * Get a 'stream' level. + */ public function getStream() : string { return $this->get(Table::ACTION_STREAM); } + /** + * Get a 'create' level. + */ public function getCreate() : string { return $this->get(Table::ACTION_CREATE); } + /** + * Get an 'edit' level. + */ public function getEdit() : string { return $this->get(Table::ACTION_EDIT); } + /** + * Get a 'delete' level. + */ public function getDelete() : string { return $this->get(Table::ACTION_DELETE); diff --git a/application/Espo/Core/Acl/Table.php b/application/Espo/Core/Acl/Table.php index 244399cf26..e499a26175 100644 --- a/application/Espo/Core/Acl/Table.php +++ b/application/Espo/Core/Acl/Table.php @@ -39,6 +39,7 @@ use Espo\Core\{ Utils\Metadata, Utils\FieldUtil, Utils\DataCache, + Utils\ObjectUtil, }; use StdClass; @@ -170,22 +171,22 @@ class Table public function getMap() : StdClass { - return $this->data; + return ObjectUtil::clone($this->data); } - public function getScopeData(string $scope) + public function getScopeData(string $scope) : ScopeData { - if (isset($this->data->table->$scope)) { - $data = $this->data->table->$scope; - - if (is_string($data)) { - return $this->getScopeData($data); - } - - return $data; + if (!isset($this->data->table->$scope)) { + return ScopeData::fromRaw(false); } - return null; + $data = $this->data->table->$scope; + + if (is_string($data)) { + return $this->getScopeData($data); + } + + return ScopeData::fromRaw($data); } public function get(string $permission) : ?string diff --git a/application/Espo/Core/AclManager.php b/application/Espo/Core/AclManager.php index d447182e18..9bb7555947 100644 --- a/application/Espo/Core/AclManager.php +++ b/application/Espo/Core/AclManager.php @@ -29,21 +29,23 @@ namespace Espo\Core; -use Espo\Core\Exceptions\Error; - use Espo\ORM\Entity; use Espo\Entities\User; use Espo\Core\{ - Utils\ClassFinder, ORM\EntityManager, - Acl\Acl as BaseAcl, + Acl\AclFactory, Acl\ScopeAcl, Acl\GlobalRestrictonFactory, Acl\GlobalRestricton, Acl as UserAclWrapper, Acl\Table as Table, + Acl\EntityCreateAcl, + Acl\EntityReadAcl, + Acl\EntityEditAcl, + Acl\EntityDeleteAcl, + Acl\EntityStreamAcl, }; use StdClass; @@ -53,7 +55,7 @@ use StdClass; */ class AclManager { - private $implementationHashMap = []; + protected $implementationHashMap = []; private $tableHashMap = []; @@ -61,61 +63,60 @@ class AclManager protected $userAclClassName = UserAclWrapper::class; - protected $baseImplementationClassName = BaseAcl::class; + protected const PERMISSION_ASSIGNMENT = 'assignment'; - protected $globalRestricton; + private $actionInterfaceMap = [ + Table::ACTION_CREATE => EntityCreateAcl::class, + Table::ACTION_READ => EntityReadAcl::class, + Table::ACTION_EDIT => EntityEditAcl::class, + Table::ACTION_DELETE => EntityDeleteAcl::class, + Table::ACTION_STREAM => EntityStreamAcl::class, + ]; protected $injectableFactory; - protected $classFinder; - protected $entityManager; + protected $aclFactory; + + protected $globalRestricton; + public function __construct( InjectableFactory $injectableFactory, - ClassFinder $classFinder, EntityManager $entityManager, + AclFactory $aclFactory, GlobalRestrictonFactory $globalRestrictonFactory ) { $this->injectableFactory = $injectableFactory; - $this->classFinder = $classFinder; $this->entityManager = $entityManager; + $this->aclFactory = $aclFactory; $this->globalRestricton = $globalRestrictonFactory->create(); } - public function getImplementation(string $scope) : ScopeAcl + /** + * Get an ACL implementation for a scope. + * + * @return ScopeAcl|EntityAcl + */ + public function getImplementation(string $scope): ScopeAcl { - if (empty($this->implementationHashMap[$scope])) { - $className = $this->classFinder->find('Acl', $scope); - - if (!$className) { - $className = $this->baseImplementationClassName; - } - - if (!class_exists($className)) { - throw new Error("{$className} does not exist."); - } - - $acl = $this->injectableFactory->createWith($className, [ - 'scope' => $scope, - ]); - - $this->implementationHashMap[$scope] = $acl; + if (!array_key_exists($scope, $this->implementationHashMap)) { + $this->implementationHashMap[$scope] = $this->aclFactory->create($scope); } return $this->implementationHashMap[$scope]; } - protected function getTable(User $user) : Table + protected function getTable(User $user): Table { - $key = $user->id; + $key = $user->getId(); - if (empty($key)) { + if (!$key) { $key = spl_object_hash($user); } - if (empty($this->tableHashMap[$key])) { + if (!array_key_exists($key, $this->tableHashMap)) { $this->tableHashMap[$key] = $this->injectableFactory->createWith($this->tableClassName, [ 'user' => $user, ]); @@ -124,7 +125,7 @@ class AclManager return $this->tableHashMap[$key]; } - public function getMap(User $user) : StdClass + public function getMap(User $user): StdClass { return $this->getTable($user)->getMap(); } @@ -132,7 +133,7 @@ class AclManager /** * Get an access level for a specific scope and action. */ - public function getLevel(User $user, string $scope, string $action) : string + public function getLevel(User $user, string $scope, string $action): string { if ($user->isAdmin()) { return $this->getTable($user)->getHighestLevel($scope, $action); @@ -144,7 +145,7 @@ class AclManager /** * Get a permission. E.g. 'assignment' permission. */ - public function get(User $user, string $permission) : ?string + public function get(User $user, string $permission): ?string { if (substr($permission, -10) !== 'Permission') { $permission .= 'Permission'; @@ -156,49 +157,39 @@ class AclManager /** * Whether there's no 'read' access for a specific scope. */ - public function checkReadNo(User $user, string $scope) : bool + public function checkReadNo(User $user, string $scope): bool { - if ($user->isAdmin()) { - return false; - } - $data = $this->getTable($user)->getScopeData($scope); - return (bool) $this->getImplementation($scope)->checkReadNo($user, $data); + return $this->getImplementation($scope)->checkReadNo($user, $data); } /** * Whether 'read' access is set to 'team' for a specific scope. */ - public function checkReadOnlyTeam(User $user, string $scope) : bool + public function checkReadOnlyTeam(User $user, string $scope): bool { - if ($user->isAdmin()) { - return false; - } - $data = $this->getTable($user)->getScopeData($scope); - return (bool) $this->getImplementation($scope)->checkReadOnlyTeam($user, $data); + return $this->getImplementation($scope)->checkReadOnlyTeam($user, $data); } /** * Whether 'read' access is set to 'own' for a specific scope. */ - public function checkReadOnlyOwn(User $user, string $scope) : bool + public function checkReadOnlyOwn(User $user, string $scope): bool { - if ($user->isAdmin()) { - return false; - } - $data = $this->getTable($user)->getScopeData($scope); - return (bool) $this->getImplementation($scope)->checkReadOnlyOwn($user, $data); + return $this->getImplementation($scope)->checkReadOnlyOwn($user, $data); } /** * Check a scope or entity. If $action is omitted, it will check whether a scope level is set to 'enabled'. + * + * @param string|Entity $subject An entity type or entity. */ - public function check(User $user, $subject, ?string $action = null) : bool + public function check(User $user, $subject, ?string $action = null): bool { if (is_string($subject)) { return $this->checkScope($user, $subject, $action); @@ -216,9 +207,9 @@ class AclManager } /** - * Check access to a specific entity (record). + * Check access to a specific entity. */ - public function checkEntity(User $user, Entity $entity, string $action = Table::ACTION_READ) : bool + public function checkEntity(User $user, Entity $entity, string $action = Table::ACTION_READ): bool { $scope = $entity->getEntityType(); @@ -232,17 +223,64 @@ class AclManager $methodName = 'checkEntity' . ucfirst($action); - if (method_exists($impl, $methodName)) { - return (bool) $impl->$methodName($user, $entity, $data); + $interface = $this->actionInterfaceMap[$action] ?? null; + + if ($interface && $impl instanceof $interface) { + return $impl->$methodName($user, $entity, $data); } - return (bool) $impl->checkEntity($user, $entity, $data, $action); + if (method_exists($impl, $methodName)) { + // For backward compatibility. + return $impl->$methodName($user, $entity, $data); + } + + return $impl->checkEntity($user, $entity, $data, $action); + } + + /** + * Check 'read' access to a specific entity. + */ + public function checkEntityRead(User $user, Entity $entity): bool + { + return $this->checkEntity($user, $entity, Table::ACTION_READ); + } + + /** + * Check 'create' access to a specific entity. + */ + public function checkEntityCreate(User $user, Entity $entity): bool + { + return $this->checkEntity($user, $entity, Table::ACTION_CREATE); + } + + /** + * Check 'edit' access to a specific entity. + */ + public function checkEntityEdit(User $user, Entity $entity): bool + { + return $this->checkEntity($user, $entity, Table::ACTION_EDIT); + } + + /** + * Check 'delete' access to a specific entity. + */ + public function checkEntityDelete(User $user, Entity $entity): bool + { + return $this->checkEntity($user, $entity, Table::ACTION_DELETE); + } + + /** + * Check 'stream' access to a specific entity. + */ + public function checkEntityStream(User $user, Entity $entity): bool + { + return $this->checkEntity($entity, Table::ACTION_STREAM); } /** * Whether a user is owned of an entity (record). Usually 'assignedUser' field is used for checking. */ - public function checkIsOwner(User $user, Entity $entity) : bool + public function checkIsOwner(User $user, Entity $entity): bool { return (bool) $this->getImplementation($entity->getEntityType())->checkIsOwner($user, $entity); } @@ -250,7 +288,7 @@ class AclManager /** * Whether a user team list overlaps with teams set in an entity. */ - public function checkInTeam(User $user, Entity $entity) : bool + public function checkInTeam(User $user, Entity $entity): bool { return (bool) $this->getImplementation($entity->getEntityType())->checkInTeam($user, $entity); } @@ -258,17 +296,17 @@ class AclManager /** * Check access to scope. If $action is omitted, it will check whether a scope level is set to 'enabled'. */ - public function checkScope(User $user, string $scope, ?string $action = null) : bool + public function checkScope(User $user, string $scope, ?string $action = null): bool { $data = $this->getTable($user)->getScopeData($scope); - return (bool) $this->getImplementation($scope)->checkScope($user, $data, $action); + return $this->getImplementation($scope)->checkScope($user, $data, $action); } /** * @deprecated Use checkUserPermission instead. */ - public function checkUser(User $user, string $permission, User $target) : bool + public function checkUser(User $user, string $permission, User $target): bool { if ($this->get($user, $permission) === Table::LEVEL_ALL) { return true; @@ -309,7 +347,7 @@ class AclManager return false; } - protected function getGlobalRestrictionTypeList(User $user, string $action = Table::ACTION_READ) : array + protected function getGlobalRestrictionTypeList(User $user, string $action = Table::ACTION_READ): array { $typeList = [ GlobalRestricton::TYPE_FORBIDDEN, @@ -338,8 +376,11 @@ class AclManager * Get attributes forbidden for a user. */ public function getScopeForbiddenAttributeList( - User $user, string $scope, string $action = Table::ACTION_READ, string $thresholdLevel = Table::LEVEL_NO - ) : array { + User $user, + string $scope, + string $action = Table::ACTION_READ, + string $thresholdLevel = Table::LEVEL_NO + ): array { $list = []; @@ -350,7 +391,10 @@ class AclManager if ($thresholdLevel === Table::LEVEL_NO) { $list = array_merge( $list, - $this->getScopeRestrictedAttributeList($scope, $this->getGlobalRestrictionTypeList($user, $action)) + $this->getScopeRestrictedAttributeList( + $scope, + $this->getGlobalRestrictionTypeList($user, $action) + ) ); $list = array_values($list); @@ -363,8 +407,11 @@ class AclManager * Get fields forbidden for a user. */ public function getScopeForbiddenFieldList( - User $user, string $scope, string $action = Table::ACTION_READ, string $thresholdLevel = Table::LEVEL_NO - ) : array { + User $user, + string $scope, + string $action = Table::ACTION_READ, + string $thresholdLevel = Table::LEVEL_NO + ): array { $list = []; @@ -375,7 +422,10 @@ class AclManager if ($thresholdLevel === Table::LEVEL_NO) { $list = array_merge( $list, - $this->getScopeRestrictedFieldList($scope, $this->getGlobalRestrictionTypeList($user, $action)) + $this->getScopeRestrictedFieldList( + $scope, + $this->getGlobalRestrictionTypeList($user, $action) + ) ); $list = array_values($list); @@ -388,15 +438,21 @@ class AclManager * Get links forbidden for a user. */ public function getScopeForbiddenLinkList( - User $user, string $scope, string $action = Table::ACTION_READ, string $thresholdLevel = Table::LEVEL_NO - ) : array { + User $user, + string $scope, + string $action = Table::ACTION_READ, + string $thresholdLevel = Table::LEVEL_NO + ): array { $list = []; if ($thresholdLevel === Table::LEVEL_NO) { $list = array_merge( $list, - $this->getScopeRestrictedLinkList($scope, $this->getGlobalRestrictionTypeList($user, $action)) + $this->getScopeRestrictedLinkList( + $scope, + $this->getGlobalRestrictionTypeList($user, $action) + ) ); $list = array_values($list); @@ -410,13 +466,14 @@ class AclManager * * @param User|string $target User entity or user ID. */ - public function checkUserPermission(User $user, $target, string $permissionType = 'user') : bool + public function checkUserPermission(User $user, $target, string $permissionType = 'user'): bool { $permission = $this->get($user, $permissionType); if (is_object($target)) { $userId = $target->id; - } else { + } + else { $userId = $target; } @@ -435,7 +492,11 @@ class AclManager if ($permission === Table::LEVEL_TEAM) { $teamIdList = $user->getLinkMultipleIdList('teams'); - if (!$this->entityManager->getRepository('User')->checkBelongsToAnyOfTeams($userId, $teamIdList)) { + if ( + !$this->entityManager + ->getRepository('User') + ->checkBelongsToAnyOfTeams($userId, $teamIdList) + ) { return false; } } @@ -448,23 +509,24 @@ class AclManager * * @param User|string $target User entity or user ID. */ - public function checkAssignmentPermission(User $user, $target) : bool + public function checkAssignmentPermission(User $user, $target): bool { - return $this->checkUserPermission($user, $target, 'assignment'); + return $this->checkUserPermission($user, $target, self::PERMISSION_ASSIGNMENT); } /** * Create a wrapper for a specific user. */ - public function createUserAcl(User $user) : UserAclWrapper + public function createUserAcl(User $user): UserAclWrapper { $className = $this->userAclClassName; + $acl = new $className($this, $user); return $acl; } - public function getScopeRestrictedFieldList(string $scope, $type) : array + public function getScopeRestrictedFieldList(string $scope, $type): array { if (is_array($type)) { $typeList = $type; @@ -481,7 +543,7 @@ class AclManager return $this->globalRestricton->getScopeRestrictedFieldList($scope, $type); } - public function getScopeRestrictedAttributeList(string $scope, $type) : array + public function getScopeRestrictedAttributeList(string $scope, $type): array { if (is_array($type)) { $typeList = $type; @@ -498,7 +560,7 @@ class AclManager return $this->globalRestricton->getScopeRestrictedAttributeList($scope, $type); } - public function getScopeRestrictedLinkList(string $scope, $type) : array + public function getScopeRestrictedLinkList(string $scope, $type): array { if (is_array($type)) { $typeList = $type; diff --git a/application/Espo/Core/AclPortal/AclFactory.php b/application/Espo/Core/AclPortal/AclFactory.php new file mode 100644 index 0000000000..12a8833215 --- /dev/null +++ b/application/Espo/Core/AclPortal/AclFactory.php @@ -0,0 +1,63 @@ +classFinder = $classFinder; + $this->injectableFactory = $injectableFactory; + } + + public function create(string $scope) : PortalScopeAcl + { + $className = $this->classFinder->find('AclPortal', $scope); + + if (!$className) { + $className = $this->baseClassName; + } + + return $this->injectableFactory->createWith($className, [ + 'scope' => $scope, // todo remove ? + ]); + } +} diff --git a/application/Espo/Core/AclPortal/Portal.php b/application/Espo/Core/AclPortal/Portal.php index 8b922c51f3..a3f2e0011c 100644 --- a/application/Espo/Core/AclPortal/Portal.php +++ b/application/Espo/Core/AclPortal/Portal.php @@ -30,29 +30,28 @@ namespace Espo\Core\AclPortal; use Espo\Entities\User; + use Espo\ORM\Entity; +use Espo\Core\{ + Acl\ScopeData, +}; + trait Portal { - public function checkScope(User $user, $data, $action = null, Entity $entity = null, $entityAccessData = []) - { - if ($user->isAdmin()) { - return true; - } + protected function checkScopeInternal( + User $user, + ScopeData $data, + ?string $action = null, + ?Entity $entity = null, + array $entityAccessData = [] + ) : bool { - if (is_null($data)) { + if ($data->isFalse()) { return false; } - if ($data === false) { - return false; - } - - if ($data === true) { - return true; - } - - if (is_string($data)) { + if ($data->isTrue()) { return true; } @@ -78,13 +77,9 @@ trait Portal return true; } - if (!isset($data->$action)) { - return false; - } + $value = $data->get($action); - $value = $data->$action; - - if ($value === Table::LEVEL_ALL || $value === Table::LEVEL_YES || $value === true) { + if ($value === Table::LEVEL_ALL || $value === Table::LEVEL_YES) { return true; } @@ -115,14 +110,13 @@ trait Portal if ($inAccount) { return true; } - else { - if (is_null($isOwnContact) && $entity) { - $isOwnContact = $this->checkIsOwnContact($user, $entity); - } - if ($isOwnContact) { - return true; - } + if (is_null($isOwnContact) && $entity) { + $isOwnContact = $this->checkIsOwnContact($user, $entity); + } + + if ($isOwnContact) { + return true; } } @@ -139,24 +133,19 @@ trait Portal return false; } - public function checkReadOnlyAccount(User $user, $data) + public function checkReadOnlyAccount(User $user, ScopeData $data) : bool { - if (empty($data) || !is_object($data) || !isset($data->read)) { - return false; - } - - return $data->read === Table::LEVEL_ACCOUNT; + return $data->getRead() === Table::LEVEL_ACCOUNT; } - public function checkReadOnlyContact(User $user, $data) + public function checkReadOnlyContact(User $user, ScopeData $data) : bool { - if (empty($data) || !is_object($data) || !isset($data->read)) { - return false; - } - - return $data->read === Table::LEVEL_CONTACT; + return $data->getRead() === Table::LEVEL_CONTACT; } + /** + * @return bool + */ public function checkIsOwner(User $user, Entity $entity) { if ($entity->hasAttribute('createdById')) { @@ -168,6 +157,9 @@ trait Portal return false; } + /** + * @return bool + */ public function checkInAccount(User $user, Entity $entity) { $accountIdList = $user->getLinkMultipleIdList('accounts'); @@ -204,6 +196,9 @@ trait Portal return false; } + /** + * @return bool + */ public function checkIsOwnContact(User $user, Entity $entity) { $contactId = $user->get('contactId'); diff --git a/application/Espo/Core/Portal/Acl.php b/application/Espo/Core/Portal/Acl.php index 019cbe860a..3c1f6f6c1c 100644 --- a/application/Espo/Core/Portal/Acl.php +++ b/application/Espo/Core/Portal/Acl.php @@ -35,22 +35,22 @@ use Espo\Core\Acl as BaseAcl; class Acl extends BaseAcl { - public function checkReadOnlyAccount(string $scope) : bool + public function checkReadOnlyAccount(string $scope): bool { return $this->aclManager->checkReadOnlyAccount($this->user, $scope); } - public function checkReadOnlyContact(string $scope) : bool + public function checkReadOnlyContact(string $scope): bool { return $this->aclManager->checkReadOnlyContact($this->user, $scope); } - public function checkInAccount(Entity $entity) : bool + public function checkInAccount(Entity $entity): bool { return $this->aclManager->checkInAccount($this->user, $entity); } - public function checkIsOwnContact(Entity $entity) : bool + public function checkIsOwnContact(Entity $entity): bool { return $this->aclManager->checkIsOwnContact($this->user, $entity); } diff --git a/application/Espo/Core/Portal/AclManager.php b/application/Espo/Core/Portal/AclManager.php index a97cde0e4d..4acc18bb14 100644 --- a/application/Espo/Core/Portal/AclManager.php +++ b/application/Espo/Core/Portal/AclManager.php @@ -31,18 +31,22 @@ namespace Espo\Core\Portal; use Espo\ORM\Entity; -use Espo\Entities\User; -use Espo\Entities\Portal; +use Espo\Entities\{ + User, + Portal, +}; use Espo\Core\{ - Exceptions\Error, AclPortal\Table as Table, AclPortal\Acl as BasePortalAcl, - AclPortal\PortalScopeAcl, + AclPortal\AclFactory as PortalAclFactory, Acl\ScopeAcl, + Acl\GlobalRestrictonFactory, Acl\Table as TableBase, Portal\Acl as UserAclWrapper, AclManager as BaseAclManager, + InjectableFactory, + ORM\EntityManager, }; use StdClass; @@ -52,57 +56,44 @@ class AclManager extends BaseAclManager { protected $tableClassName = Table::class; - private $mainManager = null; - - private $portal = null; - protected $userAclClassName = UserAclWrapper::class; protected $baseImplementationClassName = BasePortalAcl::class; - public function getImplementation(string $scope) : ScopeAcl + private $mainManager = null; + + private $portal = null; + + public function __construct( + InjectableFactory $injectableFactory, + EntityManager $entityManager, + PortalAclFactory $aclFactory, + GlobalRestrictonFactory $globalRestrictonFactory, + BaseAclManager $mainManager + ) { + $this->injectableFactory = $injectableFactory; + $this->entityManager = $entityManager; + $this->aclFactory = $aclFactory; + $this->mainManager = $mainManager; + + $this->globalRestricton = $globalRestrictonFactory->create(); + } + + public function getImplementation(string $scope): ScopeAcl { - if (empty($this->implementationHashMap[$scope])) { - $className = $this->classFinder->find('AclPortal', $scope); - - if (!$className) { - $className = $this->baseImplementationClassName; - } - - if (!class_exists($className)) { - throw new Error("{$className} does not exist."); - } - - $acl = $this->injectableFactory->createWith($className, [ - 'scope' => $scope, - ]); - - $this->implementationHashMap[$scope] = $acl; - - if (!$acl instanceof PortalScopeAcl) { - throw new Error("Portal\AclManager: Implementation should be instance of PortalScopeAcl."); - } + if (!array_key_exists($scope, $this->implementationHashMap)) { + $this->implementationHashMap[$scope] = $this->aclFactory->create($scope); } return $this->implementationHashMap[$scope]; } - public function setMainManager(BaseAclManager $mainManager) : void - { - $this->mainManager = $mainManager; - } - - protected function getMainManager() : BaseAclManager - { - return $this->mainManager; - } - - public function setPortal(Portal $portal) : void + public function setPortal(Portal $portal): void { $this->portal = $portal; } - protected function getPortal() : Portal + protected function getPortal(): Portal { if (!$this->portal) { throw new RuntimeException("Portal is not set."); @@ -111,7 +102,7 @@ class AclManager extends BaseAclManager return $this->portal; } - protected function getTable(User $user) : TableBase + protected function getTable(User $user): TableBase { $key = $user->id; @@ -129,175 +120,173 @@ class AclManager extends BaseAclManager return $this->tableHashMap[$key]; } - public function checkReadOnlyAccount(User $user, string $scope) : bool + public function checkReadOnlyAccount(User $user, string $scope): bool { - if ($user->isAdmin()) { - return false; - } - $data = $this->getTable($user)->getScopeData($scope); return $this->getImplementation($scope)->checkReadOnlyAccount($user, $data); } - public function checkReadOnlyContact(User $user, string $scope) : bool + public function checkReadOnlyContact(User $user, string $scope): bool { - if ($user->isAdmin()) { - return false; - } - $data = $this->getTable($user)->getScopeData($scope); return $this->getImplementation($scope)->checkReadOnlyContact($user, $data); } - public function checkInAccount(User $user, Entity $entity) : bool + public function checkInAccount(User $user, Entity $entity): bool { return (bool) $this->getImplementation($entity->getEntityType())->checkInAccount($user, $entity); } - public function checkIsOwnContact(User $user, Entity $entity) : bool + public function checkIsOwnContact(User $user, Entity $entity): bool { return (bool) $this->getImplementation($entity->getEntityType())->checkIsOwnContact($user, $entity); } - public function getMap(User $user) : StdClass + public function getMap(User $user): StdClass { if ($this->checkUserIsNotPortal($user)) { - return $this->getMainManager()->getMap($user); + return $this->mainManager->getMap($user); } return parent::getMap($user); } - public function getLevel(User $user, string $scope, string $action) : string + public function getLevel(User $user, string $scope, string $action): string { if ($this->checkUserIsNotPortal($user)) { - return $this->getMainManager()->getLevel($user, $scope, $action); + return $this->mainManager->getLevel($user, $scope, $action); } return parent::getLevel($user, $scope, $action); } - public function get(User $user, string $permission) : ?string + public function get(User $user, string $permission): ?string { if ($this->checkUserIsNotPortal($user)) { - return $this->getMainManager()->get($user, $permission); + return $this->mainManager->get($user, $permission); } return parent::get($user, $permission); } - public function checkReadOnlyTeam(User $user, string $scope) : bool + public function checkReadOnlyTeam(User $user, string $scope): bool { if ($this->checkUserIsNotPortal($user)) { $data = $this->getTable($user)->getScopeData($scope); - return $this->getMainManager()->checkReadOnlyTeam($user, $data); + return $this->mainManager->checkReadOnlyTeam($user, $data); } return parent::checkReadOnlyTeam($user, $scope); } - public function checkReadNo(User $user, string $scope) : bool + public function checkReadNo(User $user, string $scope): bool { if ($this->checkUserIsNotPortal($user)) { $data = $this->getTable($user)->getScopeData($scope); - return $this->getMainManager()->checkReadNo($user, $data); + return $this->mainManager->checkReadNo($user, $data); } return parent::checkReadNo($user, $scope); } - public function checkReadOnlyOwn(User $user, string $scope) : bool + public function checkReadOnlyOwn(User $user, string $scope): bool { if ($this->checkUserIsNotPortal($user)) { $data = $this->getTable($user)->getScopeData($scope); - return $this->getMainManager()->checkReadOnlyOwn($user, $data); + return $this->mainManager->checkReadOnlyOwn($user, $data); } return parent::checkReadOnlyOwn($user, $scope); } - public function check(User $user, $subject, ?string $action = null) : bool + public function check(User $user, $subject, ?string $action = null): bool { if ($this->checkUserIsNotPortal($user)) { - return $this->getMainManager()->check($user, $subject, $action); + return $this->mainManager->check($user, $subject, $action); } return parent::check($user, $subject, $action); } - public function checkEntity(User $user, Entity $entity, string $action = Table::ACTION_READ) : bool + public function checkEntity(User $user, Entity $entity, string $action = Table::ACTION_READ): bool { if ($this->checkUserIsNotPortal($user)) { - return $this->getMainManager()->checkEntity($user, $entity, $action); + return $this->mainManager->checkEntity($user, $entity, $action); } return parent::checkEntity($user, $entity, $action); } - public function checkIsOwner(User $user, Entity $entity) : bool + public function checkIsOwner(User $user, Entity $entity): bool { if ($this->checkUserIsNotPortal($user)) { - return $this->getMainManager()->checkIsOwner($user, $entity); + return $this->mainManager->checkIsOwner($user, $entity); } return parent::checkIsOwner($user, $entity); } - public function checkInTeam(User $user, Entity $entity) : bool + public function checkInTeam(User $user, Entity $entity): bool { if ($this->checkUserIsNotPortal($user)) { - return $this->getMainManager()->checkInTeam($user, $entity); + return $this->mainManager->checkInTeam($user, $entity); } return parent::checkInTeam($user, $entity); } - public function checkScope(User $user, string $scope, ?string $action = null) : bool + public function checkScope(User $user, string $scope, ?string $action = null): bool { if ($this->checkUserIsNotPortal($user)) { - return $this->getMainManager()->checkScope($user, $scope, $action); + return $this->mainManager->checkScope($user, $scope, $action); } return parent::checkScope($user, $scope, $action); } - public function checkUser(User $user, string $permission, User $entity) : bool + public function checkUser(User $user, string $permission, User $entity): bool { if ($this->checkUserIsNotPortal($user)) { - return $this->getMainManager()->checkUser($user, $permission, $entity); + return $this->mainManager->checkUser($user, $permission, $entity); } return parent::checkUser($user, $permission, $entity); } public function getScopeForbiddenAttributeList( - User $user, string $scope, string $action = Table::ACTION_READ, string $thresholdLevel = Table::LEVEL_NO - ) : array { + User $user, + string $scope, + string $action = Table::ACTION_READ, + string $thresholdLevel = Table::LEVEL_NO + ): array { if ($this->checkUserIsNotPortal($user)) { - return $this->getMainManager()->getScopeForbiddenAttributeList($user, $scope, $action, $thresholdLevel); + return $this->mainManager->getScopeForbiddenAttributeList($user, $scope, $action, $thresholdLevel); } return parent::getScopeForbiddenAttributeList($user, $scope, $action, $thresholdLevel); } public function getScopeForbiddenFieldList( - User $user, string $scope, string $action = Table::ACTION_READ, string $thresholdLevel = Table::LEVEL_NO - ) : array { + User $user, + string $scope, + string $action = Table::ACTION_READ, + string $thresholdLevel = Table::LEVEL_NO + ): array { if ($this->checkUserIsNotPortal($user)) { - return $this->getMainManager()->getScopeForbiddenFieldList($user, $scope, $action, $thresholdLevel); + return $this->mainManager->getScopeForbiddenFieldList($user, $scope, $action, $thresholdLevel); } return parent::getScopeForbiddenFieldList($user, $scope, $action, $thresholdLevel); } - protected function checkUserIsNotPortal(User $user) : bool + protected function checkUserIsNotPortal(User $user): bool { return !$user->isPortal(); } diff --git a/application/Espo/Core/Portal/Loaders/AclManager.php b/application/Espo/Core/Portal/Loaders/AclManager.php index b0a25c0361..7601b796bc 100644 --- a/application/Espo/Core/Portal/Loaders/AclManager.php +++ b/application/Espo/Core/Portal/Loaders/AclManager.php @@ -50,10 +50,8 @@ class AclManager implements Loader public function load() : PortalAclManager { - $aclManager = $this->injectableFactory->create(PortalAclManager::class); - - $aclManager->setMainManager($this->internalAclManager); - - return $aclManager; + return $this->injectableFactory->createWith(PortalAclManager::class,[ + 'mainManager' => $this->internalAclManager, + ]); } } diff --git a/application/Espo/Entities/Note.php b/application/Espo/Entities/Note.php index b4f4cd630e..92ad65110b 100644 --- a/application/Espo/Entities/Note.php +++ b/application/Espo/Entities/Note.php @@ -29,42 +29,99 @@ namespace Espo\Entities; -class Note extends \Espo\Core\ORM\Entity +use Espo\Core\ORM\Entity; + +class Note extends Entity { + public const TARGET_SELF = 'self'; + + public const TARGET_ALL = 'all'; + + public const TARGET_TEAMS = 'teams'; + + public const TARGET_USERS = 'users'; + + public const TARGET_PORTALS = 'portals'; + + public const TYPE_POST = 'Post'; + private $aclIsProcessed = false; - public function setAclIsProcessed() + public function isPost(): bool + { + return $this->getType() === self::TYPE_POST; + } + + public function getType(): ?string + { + return $this->get('type'); + } + + public function getTargetType(): ?string + { + return $this->get('targetType'); + } + + public function getParentType(): ?string + { + return $this->get('parentType'); + } + + public function getParentId(): ?string + { + return $this->get('parentId'); + } + + public function isInternal(): bool + { + return (bool) $this->get('isInternal'); + } + + public function setAclIsProcessed(): void { $this->aclIsProcessed = true; } - public function isAclProcessed() + public function isAclProcessed(): bool { - return $this->aclIsProcessed; + return (bool) $this->aclIsProcessed; } public function loadAttachments() { $data = $this->get('data'); + if (!empty($data) && !empty($data->attachmentsIds) && is_array($data->attachmentsIds)) { $attachmentsIds = $data->attachmentsIds; - $collection = $this->entityManager->getRepository('Attachment')->select(['id', 'name', 'type'])->order('createdAt')->where([ - 'id' => $attachmentsIds - ])->find(); - } else { + + $collection = $this->entityManager + ->getRepository('Attachment') + ->select(['id', 'name', 'type']) + ->order('createdAt') + ->where([ + 'id' => $attachmentsIds + ]) + ->find(); + } + else { $this->loadLinkMultipleField('attachments'); + return; } - $ids = array(); - $names = new \stdClass(); - $types = new \stdClass(); + $ids = []; + + $names = (object) []; + $types = (object) []; + foreach ($collection as $e) { $id = $e->id; $ids[] = $id; + $names->$id = $e->get('name'); $types->$id = $e->get('type'); } + $this->set('attachmentsIds', $ids); $this->set('attachmentsNames', $names); $this->set('attachmentsTypes', $types); @@ -73,21 +130,26 @@ class Note extends \Espo\Core\ORM\Entity public function addNotifiedUserId($userId) { $userIdList = $this->get('notifiedUserIdList'); + if (!is_array($userIdList)) { $userIdList = []; } + if (!in_array($userId, $userIdList)) { $userIdList[] = $userId; } + $this->set('notifiedUserIdList', $userIdList); } public function isUserIdNotified($userId) { $userIdList = $this->get('notifiedUserIdList'); + if (!is_array($userIdList)) { $userIdList = []; } + return in_array($userId, $userIdList); } } diff --git a/application/Espo/Modules/Crm/Acl/CampaignLogRecord.php b/application/Espo/Modules/Crm/Acl/CampaignLogRecord.php index aa6096d214..4030da9a62 100644 --- a/application/Espo/Modules/Crm/Acl/CampaignLogRecord.php +++ b/application/Espo/Modules/Crm/Acl/CampaignLogRecord.php @@ -33,9 +33,9 @@ use Espo\Entities\User; use Espo\ORM\Entity; -use Espo\Core\Acl\Base; +use Espo\Core\Acl\Acl; -class CampaignLogRecord extends Base +class CampaignLogRecord extends Acl { public function checkIsOwner(User $user, Entity $entity) { diff --git a/application/Espo/Modules/Crm/Acl/CampaignTrackingUrl.php b/application/Espo/Modules/Crm/Acl/CampaignTrackingUrl.php index a830462cb9..0cd181e835 100644 --- a/application/Espo/Modules/Crm/Acl/CampaignTrackingUrl.php +++ b/application/Espo/Modules/Crm/Acl/CampaignTrackingUrl.php @@ -33,9 +33,9 @@ use Espo\Entities\User; use Espo\ORM\Entity; -use Espo\Core\Acl\Base; +use Espo\Core\Acl\Acl; -class CampaignTrackingUrl extends Base +class CampaignTrackingUrl extends Acl { public function checkIsOwner(User $user, Entity $entity) { diff --git a/application/Espo/Modules/Crm/Acl/MassEmail.php b/application/Espo/Modules/Crm/Acl/MassEmail.php index 2262886b9b..3d10d3c636 100644 --- a/application/Espo/Modules/Crm/Acl/MassEmail.php +++ b/application/Espo/Modules/Crm/Acl/MassEmail.php @@ -33,9 +33,9 @@ use Espo\Entities\User; use Espo\ORM\Entity; -use Espo\Core\Acl\Base; +use Espo\Core\Acl\Acl; -class MassEmail extends Base +class MassEmail extends Acl { public function checkIsOwner(User $user, Entity $entity) { diff --git a/application/Espo/Modules/Crm/Acl/Meeting.php b/application/Espo/Modules/Crm/Acl/Meeting.php index df41dd0449..adb3ec787e 100644 --- a/application/Espo/Modules/Crm/Acl/Meeting.php +++ b/application/Espo/Modules/Crm/Acl/Meeting.php @@ -33,24 +33,25 @@ use Espo\Entities\User; use Espo\ORM\Entity; -use Espo\Core\Acl\Base; +use Espo\Core\{ + Acl\Acl, + Acl\ScopeData, + Acl\Table, + Acl\EntityReadAcl, +}; -class Meeting extends Base +class Meeting extends Acl implements EntityReadAcl { protected $ownerUserIdAttribute = 'usersIds'; - public function checkEntityRead(User $user, Entity $entity, $data) + public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool { - if ($this->checkEntity($user, $entity, $data, 'read')) { + if ($this->checkEntity($user, $entity, $data, Table::ACTION_READ)) { return true; } - if (!$data) { - return false; - } - - if ($data->read === 'own' || $data->read === 'team') { - if ($entity->hasLinkMultipleId('users', $user->id)) { + if ($data->getRead() === Table::LEVEL_OWN || $data->getRead() === Table::LEVEL_TEAM) { + if ($entity->hasLinkMultipleId('users', $user->getId())) { return true; } } diff --git a/application/Espo/Modules/Crm/AclPortal/KnowledgeBaseArticle.php b/application/Espo/Modules/Crm/AclPortal/KnowledgeBaseArticle.php index e2beaa90de..84b79efff9 100644 --- a/application/Espo/Modules/Crm/AclPortal/KnowledgeBaseArticle.php +++ b/application/Espo/Modules/Crm/AclPortal/KnowledgeBaseArticle.php @@ -33,13 +33,18 @@ use Espo\Entities\User as EntityUser; use Espo\ORM\Entity; -use Espo\Core\AclPortal\Acl; +use Espo\Core\{ + Acl\ScopeData, + Acl\Table, + Acl\EntityReadAcl, + AclPortal\Acl as Acl, +}; -class KnowledgeBaseArticle extends Acl +class KnowledgeBaseArticle extends Acl implements EntityReadAcl { - public function checkEntityRead(EntityUser $user, Entity $entity, $data) + public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { - if (!$this->checkEntity($user, $entity, $data, 'read')) { + if (!$this->checkEntity($user, $entity, $data, Table::ACTION_READ)) { return false; } diff --git a/application/Espo/Services/Note.php b/application/Espo/Services/Note.php index 8f557d11bb..cda0c375d7 100644 --- a/application/Espo/Services/Note.php +++ b/application/Espo/Services/Note.php @@ -30,11 +30,20 @@ namespace Espo\Services; use Espo\Core\Exceptions\Forbidden; -use Espo\Core\Exceptions\NotFound; use Espo\Core\Exceptions\BadRequest; +use Espo\Core\{ + Acl\Table as AclTable, +}; + +use Espo\Entities\Note as NoteEntity; + use Espo\ORM\Entity; +use DateTime; +use Exception; +use StdClass; + class Note extends Record { protected $noteNotificationPeriod = '1 hour'; @@ -42,6 +51,7 @@ class Note extends Record public function loadAdditionalFields(Entity $entity) { parent::loadAdditionalFields($entity); + $entity->loadAttachments(); } @@ -49,74 +59,120 @@ class Note extends Record { parent::afterCreateEntity($entity, $data); - if ($entity->get('type') === 'Post' && $entity->get('parentType') && $entity->get('parentType')) { - $preferences = $this->getEntityManager()->getEntity('Preferences', $this->getUser()->id); - if ($preferences && $preferences->get('followEntityOnStreamPost')) { - if ($this->getMetadata()->get(['scopes', $entity->get('parentType'), 'stream'])) { - $parent = $this->getEntityManager()->getEntity($entity->get('parentType'), $entity->get('parentId')); - if ($parent && !$this->getUser()->isSystem() && !$this->getUser()->isApi()) { - $this->getServiceFactory()->create('Stream')->followEntity($parent, $this->getUser()->id); - } - } - } - } + $this->processFollowAfterCreate($entity); } + protected function processFollowAfterCreate(NoteEntity $entity): void + { + $parentType = $entity->get('parentType'); + $parentId = $entity->get('parentId'); + + if ($entity->get('type') !== NoteEntity::TYPE_POST || !$parentType || !$parentId) { + return; + } + + if (!$this->metadata->get(['scopes', $parentType, 'stream'])) { + return; + } + + $preferences = $this->entityManager->getEntity('Preferences', $this->user->getId()); + + if (!$preferences) { + return; + } + + if (!$preferences->get('followEntityOnStreamPost')) { + return; + } + + $parent = $this->entityManager->getEntity($parentType, $parentId); + + if (!$parent || $this->user->isSystem() || $this->user->isApi()) { + return; + } + + $this->getStreamService()->followEntity($parent, $this->user->getId()); + } + + /** + * @param NoteEntity $entity + * @param StdClass $data + */ protected function beforeCreateEntity(Entity $entity, $data) { - if (!empty($data->parentType) && !empty($data->parentId)) { - $entity = $this->getEntityManager()->getEntity($data->parentType, $data->parentId); - if ($entity) { - if (!$this->getAcl()->check($entity, 'read')) { - throw new Forbidden(); - } + $parentType = $data->parentType ?? null; + $parentId = $data->parentId ?? null; + + if ($parentType && $parentId) { + $entity = $this->entityManager->getEntity($data->parentType, $data->parentId); + + if ($entity && !$this->acl->check($entity, AclTable::ACTION_READ)) { + throw new Forbidden(); } } parent::beforeCreateEntity($entity, $data); - if ($entity->get('type') === 'Post') { + if ($entity->isPost()) { $this->handlePostText($entity); } - $targetType = $entity->get('targetType'); + $targetType = $entity->getTargetType(); $entity->clear('isGlobal'); switch ($targetType) { - case 'all': + case NoteEntity::TARGET_ALL: + $entity->clear('usersIds'); $entity->clear('teamsIds'); $entity->clear('portalsIds'); $entity->set('isGlobal', true); + break; - case 'self': + + case NoteEntity::TARGET_SELF: + $entity->clear('usersIds'); $entity->clear('teamsIds'); $entity->clear('portalsIds'); - $entity->set('usersIds', [$this->getUser()->id]); + $entity->set('usersIds', [$this->user->id]); $entity->set('isForSelf', true); + break; - case 'users': + + case NoteEntity::TARGET_USERS: + $entity->clear('teamsIds'); $entity->clear('portalsIds'); + break; - case 'teams': + + case NoteEntity::TARGET_TEAMS: + $entity->clear('usersIds'); $entity->clear('portalsIds'); + break; - case 'portals': + + case NoteEntity::TARGET_PORTALS: + $entity->clear('usersIds'); $entity->clear('teamsIds'); + break; } } + /** + * @param NoteEntity $entity + * @param StdClass $data + */ protected function beforeUpdateEntity(Entity $entity, $data) { parent::beforeUpdateEntity($entity, $data); - if ($entity->get('type') === 'Post') { + if ($entity->isPost()) { $this->handlePostText($entity); } @@ -130,11 +186,16 @@ class Note extends Record protected function handlePostText(Entity $entity) { $post = $entity->get('post'); - if (empty($post)) return; + + if (empty($post)) { + return; + } $siteUrl = $this->getConfig()->getSiteUrl(); - $regexp = '/' . preg_quote($siteUrl, '/') . '(\/portal|\/portal\/[a-zA-Z0-9]*)?\/#([A-Z][a-zA-Z0-9]*)\/view\/([a-zA-Z0-9]*)/'; + $regexp = '/' . preg_quote($siteUrl, '/') . + '(\/portal|\/portal\/[a-zA-Z0-9]*)?\/#([A-Z][a-zA-Z0-9]*)\/view\/([a-zA-Z0-9]*)/'; + $post = preg_replace($regexp, '[\2/\3](#\2/view/\3)', $post); $entity->set('post', $post); @@ -142,73 +203,141 @@ class Note extends Record public function processAssignmentCheck(Entity $entity) { - if ($entity->isNew()) { - $targetType = $entity->get('targetType'); + if (!$entity->isNew()) { + return; + } - if ($targetType) { - $assignmentPermission = $this->getAcl()->get('assignmentPermission'); - if ($assignmentPermission === false || $assignmentPermission === 'no') { - if ($targetType !== 'self') { - throw new Forbidden('Not permitted to post to anybody except self.'); - } - } + $targetType = $entity->getTargetType(); - if ($targetType === 'teams') { - $teamIdList = $entity->get('teamsIds'); - if (empty($teamIdList) || !is_array($teamIdList)) { - throw new BadRequest(); - } - } - if ($targetType === 'users') { - $userIdList = $entity->get('usersIds'); - if (empty($userIdList) || !is_array($userIdList)) { - throw new BadRequest(); - } - } - if ($targetType === 'portals') { - $portalIdList = $entity->get('portalsIds'); - if (empty($portalIdList) || !is_array($portalIdList)) { - throw new BadRequest(); - } - if ($this->getAcl()->get('portalPermission') !== 'yes') { - throw new Forbidden('Not permitted to post to portal users.'); - } - } + if (!$targetType) { + return; + } - if ($assignmentPermission === 'team') { - if ($targetType === 'all') { - throw new Forbidden('Not permitted to post to all.'); - } + $userTeamIdList = $this->user->getTeamIdList(); - $userTeamIdList = $this->getUser()->getTeamIdList(); + $userIdList = $entity->getLinkMultipleIdList('users'); + $portalIdList = $entity->getLinkMultipleIdList('portals'); + $teamIdList = $entity->getLinkMultipleIdList('teams'); - if ($targetType === 'teams') { - if (empty($userTeamIdList)) { - throw new Forbidden('Not permitted to post to foreign teams.'); - } - foreach ($teamIdList as $teamId) { - if (!in_array($teamId, $userTeamIdList)) { - throw new Forbidden('Not permitted to post to foreign teams.'); - } - } - } else if ($targetType === 'users') { - if (empty($userTeamIdList)) { - throw new Forbidden('Not permitted to post to users from foreign teams.'); - } + $targetUserList = null; - foreach ($userIdList as $userId) { - if ($userId === $this->getUser()->id) { - continue; - } - if (!$this->getEntityManager()->getRepository('User')->checkBelongsToAnyOfTeams($userId, $userTeamIdList)) { - throw new Forbidden('Not permitted to post to users from foreign teams.'); - } - } - } + if ($targetType === NoteEntity::TARGET_USERS) { + $targetUserList = $this->entityManager + ->getRDBRepository('User') + ->select(['id', 'type']) + ->where([ + 'id' => $userIdList, + ]) + ->find(); + } + + $hasPortalTargetUser = false; + $allTargetUsersArePortal = true; + + foreach ($targetUserList as $user) { + if (!$user->isPortal()) { + $allTargetUsersArePortal = false; + } + + if ($user->isPortal()) { + $hasPortalTargetUser = true; + } + } + + $assignmentPermission = $this->acl->get('assignment'); + + if ($assignmentPermission === AclTable::LEVEL_NO) { + if ( + $targetType !== NoteEntity::TARGET_SELF && + $targetType !== NoteEntity::TARGET_PORTALS && + !( + $targetType === NoteEntity::TARGET_USERS && + count($userIdList) === 1 && + $userIdList[0] === $this->user->getId() + ) && + !( + $targetType === NoteEntity::TARGET_USERS && $allTargetUsersArePortal + ) + ) { + throw new Forbidden('Not permitted to post to anybody except self.'); + } + } + + if ($targetType === NoteEntity::TARGET_TEAMS) { + if (empty($teamIdList)) { + throw new BadRequest("No team IDS."); + } + } + + if ($targetType === NoteEntity::TARGET_USERS) { + if (empty($userIdList)) { + throw new BadRequest("No user IDs."); + } + } + + if ($targetType === NoteEntity::TARGET_PORTALS) { + if (empty($portalIdList)) { + throw new BadRequest("No portal IDs."); + } + + if ($this->acl->get('portal') !== AclTable::LEVEL_YES) { + throw new Forbidden('Not permitted to post to portal users.'); + } + } + + if ($targetType === NoteEntity::TARGET_USERS && $this->acl->get('portal') !== AclTable::LEVEL_YES) { + if ($hasPortalTargetUser) { + throw new Forbidden('Not permitted to post to portal users.'); + } + } + + if ($assignmentPermission === AclTable::LEVEL_TEAM) { + if ($targetType === NoteEntity::TARGET_ALL) { + throw new Forbidden('Not permitted to post to all.'); + } + } + + if ( + $assignmentPermission === AclTable::LEVEL_TEAM && + $targetType === NoteEntity::TARGET_TEAMS + ) { + if (empty($userTeamIdList)) { + throw new Forbidden('Not permitted to post to foreign teams.'); + } + + foreach ($teamIdList as $teamId) { + if (!in_array($teamId, $userTeamIdList)) { + throw new Forbidden("Not permitted to post to foreign teams."); + } + } + } + + if ( + $assignmentPermission === AclTable::LEVEL_TEAM && + $targetType === NoteEntity::TARGET_USERS + ) { + if (empty($userTeamIdList)) { + throw new Forbidden('Not permitted to post to users from foreign teams.'); + } + + foreach ($targetUserList as $user) { + if ($user->getId() === $this->user->getId()) { + continue; + } + + if ($user->isPortal()) { + continue; + } + + $inTeam = $this->entityManager + ->getRepository('User') + ->checkBelongsToAnyOfTeams($user->getId(), $userTeamIdList); + + if (!$inTeam) { + throw new Forbidden('Not permitted to post to users from foreign teams.'); } } } - return true; } public function link(string $id, string $link, string $foreignId) : void @@ -229,29 +358,39 @@ class Note extends Record parent::unlink($id, $link, $foreignId); } - public function processNoteAclJob($data) + public function processNoteAclJob(StdClass $data): void { $targetType = $data->targetType; $targetId = $data->targetId; - if ($targetType && $targetId && $this->getEntityManager()->hasRepository($targetType)) { - $entity = $this->getEntityManager()->getEntity($targetType, $targetId); + if ($targetType && $targetId && $this->entityManager->hasRepository($targetType)) { + $entity = $this->entityManager->getEntity($targetType, $targetId); + if ($entity) { $this->processNoteAcl($entity, true); } } } - public function processNoteAcl(Entity $entity, $forceProcessNoteNotifications = false) + public function processNoteAcl(Entity $entity, bool $forceProcessNoteNotifications = false): void { $entityType = $entity->getEntityType(); - if (in_array($entityType, ['Note', 'User', 'Team', 'Role', 'Portal', 'PortalRole'])) return; + if (in_array($entityType, ['Note', 'User', 'Team', 'Role', 'Portal', 'PortalRole'])) { + return; + } - if (!$this->getMetadata()->get(['scopes', $entityType, 'acl'])) return; - if (!$this->getMetadata()->get(['scopes', $entityType, 'object'])) return; + if (!$this->getMetadata()->get(['scopes', $entityType, 'acl'])) { + return; + } - $ownerUserIdAttribute = $this->getAclManager()->getImplementation($entityType)->getOwnerUserIdAttribute($entity); + if (!$this->getMetadata()->get(['scopes', $entityType, 'object'])) { + return; + } + + $ownerUserIdAttribute = $this->getAclManager() + ->getImplementation($entityType) + ->getOwnerUserIdAttribute($entity); $usersAttributeIsChanged = false; $teamsAttributeIsChanged = false; @@ -264,12 +403,16 @@ class Note extends Record if ($usersAttributeIsChanged || $forceProcessNoteNotifications) { if ($entity->getAttributeParam($ownerUserIdAttribute, 'isLinkMultipleIdList')) { $userLink = $entity->getAttributeParam($ownerUserIdAttribute, 'relation'); + $userIdList = $entity->getLinkMultipleIdList($userLink); - } else { + } + else { $userId = $entity->get($ownerUserIdAttribute); + if ($userId) { $userIdList = [$userId]; - } else { + } + else { $userIdList = []; } } @@ -280,62 +423,75 @@ class Note extends Record if ($entity->isAttributeChanged('teamsIds')) { $teamsAttributeIsChanged = true; } + if ($teamsAttributeIsChanged || $forceProcessNoteNotifications) { $teamIdList = $entity->getLinkMultipleIdList('teams'); } } if ($usersAttributeIsChanged || $teamsAttributeIsChanged || $forceProcessNoteNotifications) { - $noteList = $this->getEntityManager()->getRepository('Note')->where([ - 'OR' => [ - [ - 'relatedId' => $entity->id, - 'relatedType' => $entityType - ], - [ - 'parentId' => $entity->id, - 'parentType' => $entityType, - 'superParentId!=' => null, - 'relatedId' => null + $noteList = $this->entityManager + ->getRepository('Note') + ->where([ + 'OR' => [ + [ + 'relatedId' => $entity->id, + 'relatedType' => $entityType + ], + [ + 'parentId' => $entity->id, + '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 = []; + if (!empty($forceProcessNoteNotifications)) { $noteOptions['forceProcessNotifications'] = true; } $period = '-' . $this->getConfig()->get('noteNotificationPeriod', $this->noteNotificationPeriod); - $threshold = new \DateTime(); + + $threshold = new DateTime(); + $threshold->modify($period); foreach ($noteList as $note) { if (!$entity->isNew()) { try { - $createdAtDt = new \DateTime($note->get('createdAt')); + $createdAtDt = new DateTime($note->get('createdAt')); + if ($createdAtDt->getTimestamp() < $threshold->getTimestamp()) { continue; } - } catch (\Exception $e) {}; + } + catch (Exception $e) {}; } + if ($teamsAttributeIsChanged || $forceProcessNoteNotifications) { $note->set('teamsIds', $teamIdList); } + if ($usersAttributeIsChanged || $forceProcessNoteNotifications) { $note->set('usersIds', $userIdList); } - $this->getEntityManager()->saveEntity($note, $noteOptions); + + $this->entityManager->saveEntity($note, $noteOptions); } } } diff --git a/tests/unit/Espo/Core/Acl/ScopeDataTest.php b/tests/unit/Espo/Core/Acl/ScopeDataTest.php index 858e200a27..ab15dd8b5e 100644 --- a/tests/unit/Espo/Core/Acl/ScopeDataTest.php +++ b/tests/unit/Espo/Core/Acl/ScopeDataTest.php @@ -27,7 +27,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace tests\unit\Espo\Core\Api; +namespace tests\unit\Espo\Core\Acl; use Espo\Core\{ Acl\ScopeData, @@ -105,6 +105,8 @@ class ScopeDataTest extends \PHPUnit\Framework\TestCase $this->assertEquals($raw, $data->getRaw()); $this->assertNotSame($raw, $data->getRaw()); + + $this->assertTrue($data->hasNotNo()); } public function testRecordEmpty() @@ -114,5 +116,23 @@ class ScopeDataTest extends \PHPUnit\Framework\TestCase $data = ScopeData::fromRaw($raw); $this->assertEquals(Table::LEVEL_NO, $data->getDelete()); + + $this->assertFalse($data->hasNotNo()); + } + + public function testRecordOnlyNo() + { + $raw = (object) [ + Table::ACTION_CREATE => Table::LEVEL_NO, + Table::ACTION_READ => Table::LEVEL_NO, + Table::ACTION_EDIT => Table::LEVEL_NO, + Table::ACTION_DELETE => Table::LEVEL_NO, + ]; + + $data = ScopeData::fromRaw($raw); + + $this->assertEquals(Table::LEVEL_NO, $data->getDelete()); + + $this->assertFalse($data->hasNotNo()); } }