record link check
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Access;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\LinkChecker;
|
||||
use Espo\Core\Acl\LinkChecker\LinkCheckerFactory;
|
||||
use Espo\Core\Acl\Table as AclTable;
|
||||
use Espo\Core\Exceptions\Error\Body as ErrorBody;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Defs\RelationDefs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* Check access for record linking.
|
||||
*/
|
||||
class LinkCheck
|
||||
{
|
||||
/** @var array<string, LinkChecker<Entity, Entity>>> */
|
||||
private $linkCheckerCache = [];
|
||||
|
||||
/**
|
||||
* @param string[] $noEditAccessRequiredLinkList
|
||||
*/
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl,
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private User $user,
|
||||
private array $noEditAccessRequiredLinkList = [],
|
||||
private bool $noEditAccessRequiredForLink = false
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Checks relation fields set in an entity.
|
||||
*
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function process(Entity $entity): void
|
||||
{
|
||||
$this->processLinkMultiple($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function processLinkMultiple(Entity $entity): void
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$entityDefs = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType);
|
||||
|
||||
$typeList = [
|
||||
Entity::HAS_MANY,
|
||||
Entity::MANY_MANY,
|
||||
Entity::HAS_CHILDREN,
|
||||
];
|
||||
|
||||
foreach ($entityDefs->getRelationList() as $relationDefs) {
|
||||
$name = $relationDefs->getName();
|
||||
|
||||
if (!in_array($relationDefs->getType(), $typeList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attribute = $name . 'Ids';
|
||||
|
||||
if (
|
||||
!$entityDefs->hasAttribute($attribute) ||
|
||||
!$entity->isAttributeChanged($attribute)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var string[] $ids */
|
||||
$ids = $entity->get($attribute) ?? [];
|
||||
/** @var string[] $oldIds */
|
||||
$oldIds = $entity->getFetched($attribute) ?? [];
|
||||
|
||||
$ids = array_values(array_diff($ids, $oldIds));
|
||||
$removedIds = array_values(array_diff($oldIds, $ids));
|
||||
|
||||
if ($ids === [] && $removedIds === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasLinkMultiple =
|
||||
$entityDefs->hasField($name) &&
|
||||
$entityDefs->getField($name)->getType() === 'linkMultiple';
|
||||
|
||||
if (
|
||||
!$hasLinkMultiple &&
|
||||
!$this->acl->getScopeForbiddenLinkList($entityType, AclTable::ACTION_EDIT)
|
||||
) {
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No access to link {$name}.",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('cannotRelateForbiddenLink', null, ['link' => $name])
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
|
||||
if ($ids === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$this->processLinkedRecordsCheckItem($entity, $relationDefs, $id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function processLinkedRecordsCheckItem(Entity $entity, RelationDefs $defs, string $id): void
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
$link = $defs->getName();
|
||||
|
||||
if (!$defs->hasForeignEntityType()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$foreignEntityType = $defs->getForeignEntityType();
|
||||
|
||||
$foreignEntity = $this->entityManager->getEntityById($foreignEntityType, $id);
|
||||
|
||||
if (!$foreignEntity) {
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"Can't relate with non-existing record.",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation(
|
||||
'cannotRelateNonExisting', null, ['foreignEntityType' => $foreignEntityType])
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
|
||||
$entityDefs = $this->entityManager->getDefs()->getEntity($entityType);
|
||||
|
||||
$readAccess =
|
||||
$entityDefs->hasField($link) &&
|
||||
$entityDefs->getField($link)->getType() === 'linkMultiple';
|
||||
|
||||
$this->linkForeignAccessCheck($entityType, $link, $foreignEntity, true, $readAccess);
|
||||
$this->linkEntityAccessCheck($entity, $foreignEntity, $link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access to a specific link.
|
||||
*
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function processLink(Entity $entity, string $link): void
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
/** @var AclTable::ACTION_*|null $action */
|
||||
$action = $this->metadata
|
||||
->get(['recordDefs', $entityType, 'relationships', $link, 'linkRequiredAccess']) ??
|
||||
null;
|
||||
|
||||
if (!$action) {
|
||||
$action = $this->noEditAccessRequiredForLink ?
|
||||
AclTable::ACTION_READ :
|
||||
AclTable::ACTION_EDIT;
|
||||
}
|
||||
|
||||
if (!$this->acl->check($entity, $action)) {
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No record access for link operation ({$entityType}:{$link}).",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('noAccessToRecord', null, ['action' => $action])
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check link access for a specific foreign entity.
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function processLinkForeign(Entity $entity, string $link, Entity $foreignEntity): void
|
||||
{
|
||||
$this->linkForeignAccessCheck($entity->getEntityType(), $link, $foreignEntity);
|
||||
$this->linkEntityAccessCheck($entity, $foreignEntity, $link);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function linkForeignAccessCheck(
|
||||
string $entityType,
|
||||
string $link,
|
||||
Entity $foreignEntity,
|
||||
bool $fromUpdate = false,
|
||||
bool $readAccess = false
|
||||
): void {
|
||||
|
||||
$action = in_array($link, $this->noEditAccessRequiredLinkList) ?
|
||||
AclTable::ACTION_READ : null;
|
||||
|
||||
if (!$action) {
|
||||
/** @var AclTable::ACTION_* $action */
|
||||
$action = $this->metadata
|
||||
->get(['recordDefs', $entityType, 'relationships', $link, 'linkRequiredForeignAccess']) ??
|
||||
AclTable::ACTION_EDIT;
|
||||
}
|
||||
|
||||
if ($readAccess) {
|
||||
$action = AclTable::ACTION_READ;
|
||||
}
|
||||
|
||||
if ($this->acl->check($foreignEntity, $action)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$body = ErrorBody::create();
|
||||
|
||||
$body = $fromUpdate ?
|
||||
$body->withMessageTranslation('cannotRelateForbidden', null, [
|
||||
'foreignEntityType' => $foreignEntity->getEntityType(),
|
||||
'action' => $action,
|
||||
]) :
|
||||
$body->withMessageTranslation('noAccessToForeignRecord', null, ['action' => $action]);
|
||||
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No foreign record access for link operation ({$entityType}:{$link}).",
|
||||
$body->encode()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function linkEntityAccessCheck(Entity $entity, Entity $foreignEntity, string $link): void
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$checker = $this->getLinkChecker($entityType, $link);
|
||||
|
||||
if (!$checker) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($checker->check($this->user, $entity, $foreignEntity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No access for link operation ({$entityType}:{$link}).",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('noLinkAccess')
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?LinkChecker<Entity, Entity>
|
||||
*/
|
||||
private function getLinkChecker(string $entityType, string $link): ?LinkChecker
|
||||
{
|
||||
$key = $entityType . '_' . $link;
|
||||
|
||||
if (array_key_exists($key, $this->linkCheckerCache)) {
|
||||
return $this->linkCheckerCache[$key];
|
||||
}
|
||||
|
||||
$factory = $this->injectableFactory->create(LinkCheckerFactory::class);
|
||||
|
||||
if (!$factory->isCreatable($entityType, $link)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$checker = $factory->create($entityType, $link);
|
||||
|
||||
$this->linkCheckerCache[$link] = $checker;
|
||||
|
||||
return $checker;
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,8 @@
|
||||
|
||||
namespace Espo\Core\Record;
|
||||
|
||||
use Espo\Core\Acl\LinkChecker;
|
||||
use Espo\Core\Acl\LinkChecker\LinkCheckerFactory;
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\Binding\ContextualBinder;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Exceptions\Error\Body as ErrorBody;
|
||||
@@ -40,8 +40,8 @@ use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\NotFoundSilent;
|
||||
use Espo\Core\Record\Access\LinkCheck;
|
||||
use Espo\Entities\ActionHistoryRecord;
|
||||
use Espo\ORM\Defs\RelationDefs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Repository\RDBRepository;
|
||||
use Espo\ORM\Collection;
|
||||
@@ -186,8 +186,7 @@ class Service implements Crud,
|
||||
private $listLoadProcessor = null;
|
||||
/** @var ?DuplicateFinder */
|
||||
private $duplicateFinder = null;
|
||||
/** @var array<string, ?LinkChecker<TEntity, Entity>> */
|
||||
private $linkCheckerCache = [];
|
||||
private ?LinkCheck $linkCheck = null;
|
||||
|
||||
protected const MAX_SELECT_TEXT_ATTRIBUTE_LENGTH = 10000;
|
||||
|
||||
@@ -377,120 +376,26 @@ class Service implements Crud,
|
||||
return $this->assignmentCheckerManager->check($this->user, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @throws Forbidden
|
||||
* @todo Move to a separate class Access\LinkCheck.
|
||||
*/
|
||||
private function processLinkedRecordsCheck(Entity $entity): void
|
||||
private function getLinkCheck(): LinkCheck
|
||||
{
|
||||
$this->processLinkMultipleRecordsCheck($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function processLinkMultipleRecordsCheck(Entity $entity): void
|
||||
{
|
||||
$entityDefs = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($this->entityType);
|
||||
|
||||
$typeList = [
|
||||
Entity::HAS_MANY,
|
||||
Entity::MANY_MANY,
|
||||
Entity::HAS_CHILDREN,
|
||||
];
|
||||
|
||||
foreach ($entityDefs->getRelationList() as $relationDefs) {
|
||||
$name = $relationDefs->getName();
|
||||
|
||||
if (!in_array($relationDefs->getType(), $typeList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attribute = $name . 'Ids';
|
||||
|
||||
if (
|
||||
!$entityDefs->hasAttribute($attribute) ||
|
||||
!$entity->isAttributeChanged($attribute)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var string[] $ids */
|
||||
$ids = $entity->get($attribute) ?? [];
|
||||
/** @var string[] $oldIds */
|
||||
$oldIds = $entity->getFetched($attribute) ?? [];
|
||||
|
||||
$ids = array_values(array_diff($ids, $oldIds));
|
||||
$removedIds = array_values(array_diff($oldIds, $ids));
|
||||
|
||||
if ($ids === [] && $removedIds === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasLinkMultiple =
|
||||
$entityDefs->hasField($name) &&
|
||||
$entityDefs->getField($name)->getType() === 'linkMultiple';
|
||||
|
||||
if (
|
||||
!$hasLinkMultiple &&
|
||||
!$this->acl->getScopeForbiddenLinkList($this->entityType, AclTable::ACTION_EDIT)
|
||||
) {
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No access to link {$name}.",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('cannotRelateForbiddenLink', null, ['link' => $name])
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
|
||||
if ($ids === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$this->processLinkedRecordsCheckItem($entity, $relationDefs, $id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function processLinkedRecordsCheckItem(Entity $entity, RelationDefs $defs, string $id): void
|
||||
{
|
||||
$link = $defs->getName();
|
||||
|
||||
if (!$defs->hasForeignEntityType()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$foreignEntityType = $defs->getForeignEntityType();
|
||||
|
||||
$foreignEntity = $this->entityManager->getEntityById($foreignEntityType, $id);
|
||||
|
||||
if (!$foreignEntity) {
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"Can't relate with non-existing record.",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation(
|
||||
'cannotRelateNonExisting', null, ['foreignEntityType' => $foreignEntityType])
|
||||
->encode()
|
||||
if (!$this->linkCheck) {
|
||||
$linkCheck = $this->injectableFactory->createWithBinding(
|
||||
LinkCheck::class,
|
||||
BindingContainerBuilder::create()
|
||||
->bindInstance(Acl::class, $this->acl)
|
||||
->bindInstance(User::class, $this->user)
|
||||
->inContext(LinkCheck::class, function (ContextualBinder $binder) {
|
||||
$binder
|
||||
->bindValue('$noEditAccessRequiredLinkList', $this->noEditAccessRequiredLinkList)
|
||||
->bindValue('$noEditAccessRequiredForLink', $this->noEditAccessRequiredForLink);
|
||||
})
|
||||
->build()
|
||||
);
|
||||
|
||||
$this->linkCheck = $linkCheck;
|
||||
}
|
||||
|
||||
$entityDefs = $this->entityManager->getDefs()->getEntity($this->entityType);
|
||||
|
||||
$readAccess =
|
||||
$entityDefs->hasField($link) &&
|
||||
$entityDefs->getField($link)->getType() === 'linkMultiple';
|
||||
|
||||
$this->linkForeignAccessCheck($link, $foreignEntity, true, $readAccess);
|
||||
$this->linkEntityAccessCheck($entity, $foreignEntity, $link);
|
||||
return $this->linkCheck;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -756,7 +661,7 @@ class Service implements Crud,
|
||||
|
||||
$this->processValidation($entity, $data);
|
||||
$this->processAssignmentCheck($entity);
|
||||
$this->processLinkedRecordsCheck($entity);
|
||||
$this->getLinkCheck()->process($entity);
|
||||
|
||||
if (!$params->skipDuplicateCheck()) {
|
||||
$this->processDuplicateCheck($entity, $data);
|
||||
@@ -782,7 +687,7 @@ class Service implements Crud,
|
||||
*
|
||||
* @return TEntity
|
||||
* @throws NotFound If record not found.
|
||||
* @throws Forbidden If no access
|
||||
* @throws Forbidden If no access.
|
||||
* @throws Conflict
|
||||
* @throws BadRequest
|
||||
*/
|
||||
@@ -822,7 +727,7 @@ class Service implements Crud,
|
||||
|
||||
$this->processValidation($entity, $data);
|
||||
$this->processAssignmentCheck($entity);
|
||||
$this->processLinkedRecordsCheck($entity);
|
||||
$this->getLinkCheck()->process($entity);
|
||||
|
||||
$checkForDuplicates =
|
||||
$this->metadata->get(['recordDefs', $this->entityType, 'updateDuplicateCheck']) ??
|
||||
@@ -1144,7 +1049,7 @@ class Service implements Crud,
|
||||
throw new LogicException("Only core entities are supported.");
|
||||
}
|
||||
|
||||
$this->linkAccessCheck($entity, $link);
|
||||
$this->getLinkCheck()->processLink($entity, $link);
|
||||
|
||||
$methodName = 'link' . ucfirst($link);
|
||||
|
||||
@@ -1166,8 +1071,7 @@ class Service implements Crud,
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$this->linkForeignAccessCheck($link, $foreignEntity);
|
||||
$this->linkEntityAccessCheck($entity, $foreignEntity, $link);
|
||||
$this->getLinkCheck()->processLinkForeign($entity, $link, $foreignEntity);
|
||||
|
||||
$this->recordHookManager->processBeforeLink($entity, $link, $foreignEntity);
|
||||
|
||||
@@ -1205,7 +1109,7 @@ class Service implements Crud,
|
||||
throw new LogicException("Only core entities are supported.");
|
||||
}
|
||||
|
||||
$this->linkAccessCheck($entity, $link);
|
||||
$this->getLinkCheck()->processLink($entity, $link);
|
||||
|
||||
$methodName = 'unlink' . ucfirst($link);
|
||||
|
||||
@@ -1227,8 +1131,7 @@ class Service implements Crud,
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$this->linkForeignAccessCheck($link, $foreignEntity);
|
||||
$this->linkEntityAccessCheck($entity, $foreignEntity, $link);
|
||||
$this->getLinkCheck()->processLinkForeign($entity, $link, $foreignEntity);
|
||||
|
||||
$this->recordHookManager->processBeforeUnlink($entity, $link, $foreignEntity);
|
||||
|
||||
@@ -1237,123 +1140,6 @@ class Service implements Crud,
|
||||
->unrelate($foreignEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function linkAccessCheck(Entity $entity, string $link): void
|
||||
{
|
||||
/** @var AclTable::ACTION_*|null $action */
|
||||
$action = $this->metadata
|
||||
->get(['recordDefs', $this->entityType, 'relationships', $link, 'linkRequiredAccess']) ??
|
||||
null;
|
||||
|
||||
if (!$action) {
|
||||
$action = $this->noEditAccessRequiredForLink ?
|
||||
AclTable::ACTION_READ :
|
||||
AclTable::ACTION_EDIT;
|
||||
}
|
||||
|
||||
if (!$this->acl->check($entity, $action)) {
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No record access for link operation ({$this->entityType}:{$link}).",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('noAccessToRecord', null, ['action' => $action])
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function linkForeignAccessCheck(
|
||||
string $link,
|
||||
Entity $foreignEntity,
|
||||
bool $fromUpdate = false,
|
||||
bool $readAccess = false
|
||||
): void {
|
||||
|
||||
$action = in_array($link, $this->noEditAccessRequiredLinkList) ?
|
||||
AclTable::ACTION_READ : null;
|
||||
|
||||
if (!$action) {
|
||||
/** @var AclTable::ACTION_* $action */
|
||||
$action = $this->metadata
|
||||
->get(['recordDefs', $this->entityType, 'relationships', $link, 'linkRequiredForeignAccess']) ??
|
||||
AclTable::ACTION_EDIT;
|
||||
}
|
||||
|
||||
if ($readAccess) {
|
||||
$action = AclTable::ACTION_READ;
|
||||
}
|
||||
|
||||
if ($this->acl->check($foreignEntity, $action)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$body = ErrorBody::create();
|
||||
|
||||
$body = $fromUpdate ?
|
||||
$body->withMessageTranslation('cannotRelateForbidden', null, [
|
||||
'foreignEntityType' => $foreignEntity->getEntityType(),
|
||||
'action' => $action,
|
||||
]) :
|
||||
$body->withMessageTranslation('noAccessToForeignRecord', null, ['action' => $action]);
|
||||
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No foreign record access for link operation ({$this->entityType}:{$link}).",
|
||||
$body->encode()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function linkEntityAccessCheck(Entity $entity, Entity $foreignEntity, string $link): void
|
||||
{
|
||||
$checker = $this->getLinkChecker($link);
|
||||
|
||||
if (!$checker) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($checker->check($this->user, $entity, $foreignEntity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No access for link operation ({$this->entityType}:{$link}).",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('noLinkAccess')
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?LinkChecker<TEntity, Entity>
|
||||
*/
|
||||
private function getLinkChecker(string $link): ?LinkChecker
|
||||
{
|
||||
if (array_key_exists($link, $this->linkCheckerCache)) {
|
||||
return $this->linkCheckerCache[$link];
|
||||
}
|
||||
|
||||
$factory = $this->injectableFactory->create(LinkCheckerFactory::class);
|
||||
|
||||
if (!$factory->isCreatable($this->entityType, $link)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var LinkChecker<TEntity, Entity> $checker */
|
||||
$checker = $factory->create($this->entityType, $link);
|
||||
|
||||
$this->linkCheckerCache[$link] = $checker;
|
||||
|
||||
return $checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
|
||||
@@ -29,12 +29,15 @@
|
||||
|
||||
namespace Espo\Tools\MassUpdate;
|
||||
|
||||
use Espo\Core\FieldProcessing\LinkMultiple\ListLoader as LinkMultipleLoader;
|
||||
use Espo\Core\FieldProcessing\Loader\Params as LoaderParams;
|
||||
use Espo\Core\MassAction\QueryBuilder;
|
||||
use Espo\Core\MassAction\Params;
|
||||
use Espo\Core\MassAction\Result;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Record\Access\LinkCheck;
|
||||
use Espo\Core\Record\ServiceFactory;
|
||||
use Espo\Core\Record\Service;
|
||||
|
||||
@@ -54,40 +57,23 @@ use stdClass;
|
||||
|
||||
class Processor
|
||||
{
|
||||
private ValueMapPreparator $valueMapPreparator;
|
||||
|
||||
private QueryBuilder $queryBuilder;
|
||||
|
||||
private Acl $acl;
|
||||
|
||||
private ServiceFactory $serviceFactory;
|
||||
|
||||
private EntityManager $entityManager;
|
||||
|
||||
private FieldUtil $fieldUtil;
|
||||
|
||||
private User $user;
|
||||
|
||||
private const PERMISSION = 'massUpdatePermission';
|
||||
|
||||
public function __construct(
|
||||
ValueMapPreparator $valueMapPreparator,
|
||||
QueryBuilder $queryBuilder,
|
||||
Acl $acl,
|
||||
ServiceFactory $serviceFactory,
|
||||
EntityManager $entityManager,
|
||||
FieldUtil $fieldUtil,
|
||||
User $user
|
||||
) {
|
||||
$this->valueMapPreparator = $valueMapPreparator;
|
||||
$this->queryBuilder = $queryBuilder;
|
||||
$this->acl = $acl;
|
||||
$this->serviceFactory = $serviceFactory;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->fieldUtil = $fieldUtil;
|
||||
$this->user = $user;
|
||||
}
|
||||
private ValueMapPreparator $valueMapPreparator,
|
||||
private QueryBuilder $queryBuilder,
|
||||
private Acl $acl,
|
||||
private ServiceFactory $serviceFactory,
|
||||
private EntityManager $entityManager,
|
||||
private FieldUtil $fieldUtil,
|
||||
private User $user,
|
||||
private LinkCheck $linkCheck,
|
||||
private LinkMultipleLoader $linkMultipleLoader
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function process(Params $params, Data $data): Result
|
||||
{
|
||||
$entityType = $params->getEntityType();
|
||||
@@ -96,7 +82,7 @@ class Processor
|
||||
throw new Forbidden("No edit access for '{$entityType}'.");
|
||||
}
|
||||
|
||||
if ($this->acl->get(self::PERMISSION) !== Table::LEVEL_YES) {
|
||||
if ($this->acl->getPermissionLevel(self::PERMISSION) !== Table::LEVEL_YES) {
|
||||
throw new Forbidden("No mass-update permission.");
|
||||
}
|
||||
|
||||
@@ -174,12 +160,19 @@ class Processor
|
||||
|
||||
$values = $this->prepareItemValueMap($entity, $data, $i, $fieldToCopyList);
|
||||
|
||||
// Needed for link check.
|
||||
$this->linkMultipleLoader->process(
|
||||
$entity,
|
||||
LoaderParams::create()
|
||||
->withSelect($data->getAttributeList())
|
||||
);
|
||||
|
||||
$entity->set($values);
|
||||
|
||||
try {
|
||||
$service->processValidation($entity, $values);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
catch (Exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -187,6 +180,13 @@ class Processor
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->linkCheck->process($entity);
|
||||
}
|
||||
catch (Forbidden) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->entityManager->saveEntity($entity, [
|
||||
'massUpdate' => true,
|
||||
'skipStreamNotesAcl' => true,
|
||||
|
||||
Reference in New Issue
Block a user