From aa4d07953973eb6f9f1b2e352385de51246036d2 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Thu, 19 Jun 2025 11:13:32 +0300 Subject: [PATCH] loop reference check --- application/Espo/Services/RecordTree.php | 26 +++++++ .../Move/LoopReferenceChecker.php | 68 +++++++++++++++++++ .../Espo/Tools/CategoryTree/MoveService.php | 20 +----- 3 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 application/Espo/Tools/CategoryTree/Move/LoopReferenceChecker.php diff --git a/application/Espo/Services/RecordTree.php b/application/Espo/Services/RecordTree.php index e24c2afe35..7b1b4e1036 100644 --- a/application/Espo/Services/RecordTree.php +++ b/application/Espo/Services/RecordTree.php @@ -32,6 +32,7 @@ namespace Espo\Services; use Espo\Core\Acl\Table; use Espo\Core\Exceptions\BadRequest; use Espo\Core\Name\Field; +use Espo\Core\Templates\Entities\CategoryTree; use Espo\ORM\Collection; use Espo\ORM\Entity; use Espo\ORM\Name\Attribute; @@ -46,6 +47,7 @@ use Espo\Core\Select\Where\Item as WhereItem; use Espo\Core\Acl\Exceptions\NotImplemented; use ArrayAccess; +use Espo\Tools\CategoryTree\Move\LoopReferenceChecker; use stdClass; /** @@ -342,6 +344,30 @@ class RecordTree extends Record } } + /** + * @throws Forbidden + */ + protected function beforeUpdateEntity(Entity $entity, $data) + { + parent::beforeUpdateEntity($entity, $data); + + if ( + !$entity->isNew() && + $entity->isAttributeChanged('parentId') && + $entity->get('parentId') && + $entity instanceof CategoryTree + ) { + $parentId = $entity->get('parentId'); + + $parent = $this->entityManager->getEntityById($this->entityType, $parentId); + + if ($parent) { + $this->injectableFactory->create(LoopReferenceChecker::class) + ->check($entity, $parent); + } + } + } + public function update(string $id, stdClass $data, UpdateParams $params): Entity { if (!empty($data->parentId) && $data->parentId === $id) { diff --git a/application/Espo/Tools/CategoryTree/Move/LoopReferenceChecker.php b/application/Espo/Tools/CategoryTree/Move/LoopReferenceChecker.php new file mode 100644 index 0000000000..972303265e --- /dev/null +++ b/application/Espo/Tools/CategoryTree/Move/LoopReferenceChecker.php @@ -0,0 +1,68 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\CategoryTree\Move; + +use Espo\Core\Exceptions\Forbidden; +use Espo\Core\Templates\Entities\CategoryTree; +use Espo\ORM\Entity; +use Espo\ORM\EntityManager; + +class LoopReferenceChecker +{ + private const ATTR_PARENT_ID = 'parentId'; + + public function __construct( + private EntityManager $entityManager, + ) {} + + /** + * @throws Forbidden + */ + public function check(CategoryTree $entity, Entity $reference): void + { + $parentId = $reference->get(self::ATTR_PARENT_ID); + + if (!$parentId) { + return; + } + + if ($parentId === $entity->getId()) { + throw new Forbidden("Cannot move. Circle reference."); + } + + $parent = $this->entityManager->getEntityById($entity->getEntityType(), $parentId); + + if (!$parent) { + return; + } + + $this->check($entity, $parent); + } +} diff --git a/application/Espo/Tools/CategoryTree/MoveService.php b/application/Espo/Tools/CategoryTree/MoveService.php index 36f47d4843..ac8c0f4c22 100644 --- a/application/Espo/Tools/CategoryTree/MoveService.php +++ b/application/Espo/Tools/CategoryTree/MoveService.php @@ -39,6 +39,7 @@ use Espo\ORM\EntityManager; use Espo\ORM\Query\Part\Expression as Expr; use Espo\ORM\Query\UpdateBuilder; use Espo\ORM\Repository\Option\SaveOption; +use Espo\Tools\CategoryTree\Move\LoopReferenceChecker; use Espo\Tools\CategoryTree\Move\MoveParams; class MoveService @@ -49,6 +50,7 @@ class MoveService public function __construct( private EntityManager $entityManager, private Acl $acl, + private LoopReferenceChecker $loopReferenceChecker, ) {} /** @@ -212,22 +214,6 @@ class MoveService */ private function checkReferenceNoLoop(Entity $reference, CategoryTree $entity): void { - $parentId = $reference->get(self::ATTR_PARENT_ID); - - if (!$parentId) { - return; - } - - if ($parentId === $entity->getId()) { - throw new Forbidden("Cannot move. Circle reference."); - } - - $parent = $this->entityManager->getEntityById($entity->getEntityType(), $parentId); - - if (!$parent) { - return; - } - - $this->checkReferenceNoLoop($parent, $entity); + $this->loopReferenceChecker->check($entity, $reference); } }