configurator restriction (#3616)

This commit is contained in:
Yurii Kuznietsov
2026-03-25 10:20:06 +02:00
committed by GitHub
parent 8f28a9ad8d
commit 41a78dee3e
56 changed files with 2205 additions and 881 deletions

View File

@@ -0,0 +1,38 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Core\Acl\Exceptions;
use Exception;
/**
* @since 9.4.0
*/
class Restricted extends Exception
{}

View File

@@ -0,0 +1,231 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Core\Acl;
use Espo\Core\Acl\Exceptions\Restricted;
use Espo\Core\Utils\Metadata;
use Espo\Entities\AppLogRecord;
use Espo\Entities\ArrayValue;
use Espo\Entities\Extension;
use Espo\Entities\MassAction;
use Espo\Entities\PasswordChangeRequest;
use Espo\Entities\SystemData;
use Espo\Entities\TwoFactorCode;
use Espo\Entities\User;
use Espo\ORM\Entity;
/**
* @since 9.4.0
*/
class SystemRestriction
{
/** @var string[] */
private array $writeForbiddenEntityTypeList = [
Extension::ENTITY_TYPE,
AppLogRecord::ENTITY_TYPE,
PasswordChangeRequest::ENTITY_TYPE,
TwoFactorCode::ENTITY_TYPE,
SystemData::ENTITY_TYPE,
MassAction::ENTITY_TYPE,
ArrayValue::ENTITY_TYPE,
];
public function __construct(
private Metadata $metadata,
private GlobalRestriction $globalRestriction,
) {}
/**
* @throws Restricted
*/
public function assertUpdate(Entity $entity): void
{
$entityType = $entity->getEntityType();
if (!$this->checkEntityTypeWrite($entity->getEntityType())) {
throw new Restricted("Cannot write '$entityType' entity.");
}
if ($entity instanceof User) {
$this->assertUserUpdate($entity);
}
}
/**
* @throws Restricted
*/
public function assertRemoval(Entity $entity): void
{
$entityType = $entity->getEntityType();
if (!$this->checkEntityTypeWrite($entity->getEntityType())) {
throw new Restricted("Cannot remove '$entityType' entity.");
}
if ($entity instanceof User) {
$this->assertRemovalUser($entity);
}
}
public function checkEntityTypeWrite(string $entityType): bool
{
if (in_array($entityType, $this->writeForbiddenEntityTypeList)) {
return false;
}
if ($this->metadata->get("entityAcl.$entityType.systemWriteForbidden")) {
return false;
}
return true;
}
/**
* @return string[]
*/
private static function getUserRestrictedTypeList(): array
{
return [
User::TYPE_SUPER_ADMIN,
User::TYPE_SYSTEM,
];
}
/**
* @throws Restricted
*/
private function assertUserUpdate(User $entity): void
{
$restrictedTypeList = self::getUserRestrictedTypeList();
if (
$entity->isAttributeChanged(User::ATTR_TYPE) &&
(
in_array($entity->getFetched(User::ATTR_TYPE), $restrictedTypeList) ||
in_array($entity->getType(), $restrictedTypeList)
)
) {
throw new Restricted("Cannot change user type.");
}
}
/**
* @throws Restricted
*/
private function assertRemovalUser(User $entity): void
{
if (in_array($entity->getType(), self::getUserRestrictedTypeList())) {
throw new Restricted("Cannot remove {$entity->getId()} user.");
}
}
/**
* @return string[]
*/
public function getReadRestrictedAttributeList(string $entityType): array
{
$list1 = $this->globalRestriction
->getScopeRestrictedAttributeList($entityType, GlobalRestriction::TYPE_FORBIDDEN);
$list2 = $this->globalRestriction
->getScopeRestrictedAttributeList($entityType, GlobalRestriction::TYPE_INTERNAL);
$list = array_merge($list1, $list2);
$list = array_unique($list);
return array_values($list);
}
/**
* @return string[]
*/
public function getWriteRestrictedAttributeList(string $entityType): array
{
return $this->globalRestriction
->getScopeRestrictedAttributeList($entityType, GlobalRestriction::TYPE_FORBIDDEN);
}
public function checkLinkWrite(string $entityType, string $link): bool
{
$forbiddenList = $this->globalRestriction
->getScopeRestrictedLinkList($entityType, GlobalRestriction::TYPE_FORBIDDEN);
return !in_array($link, $forbiddenList);
}
public function checkLinkRead(string $entityType, string $link): bool
{
$internalList = $this->globalRestriction
->getScopeRestrictedLinkList($entityType, GlobalRestriction::TYPE_INTERNAL);
$forbiddenList = $this->globalRestriction
->getScopeRestrictedLinkList($entityType, GlobalRestriction::TYPE_FORBIDDEN);
return !in_array($link, $internalList) && !in_array($link, $forbiddenList);
}
public function checkAttributeRead(string $entityType, string $attribute): bool
{
$internalList = $this->globalRestriction
->getScopeRestrictedAttributeList($entityType, GlobalRestriction::TYPE_INTERNAL);
$forbiddenList = $this->globalRestriction
->getScopeRestrictedAttributeList($entityType, GlobalRestriction::TYPE_FORBIDDEN);
return !in_array($attribute, $internalList) && !in_array($attribute, $forbiddenList);
}
public function checkFieldRead(string $entityType, string $field): bool
{
$internalList = $this->globalRestriction
->getScopeRestrictedFieldList($entityType, GlobalRestriction::TYPE_INTERNAL);
$forbiddenList = $this->globalRestriction
->getScopeRestrictedFieldList($entityType, GlobalRestriction::TYPE_FORBIDDEN);
return !in_array($field, $internalList) && !in_array($field, $forbiddenList);
}
public function checkFieldWrite(string $entityType, string $field): bool
{
$forbiddenList = $this->globalRestriction
->getScopeRestrictedFieldList($entityType, GlobalRestriction::TYPE_FORBIDDEN);
return !in_array($field, $forbiddenList);
}
public function checkAttributeWrite(string $entityType, string $attribute): bool
{
$forbiddenList = $this->globalRestriction
->getScopeRestrictedAttributeList($entityType, GlobalRestriction::TYPE_FORBIDDEN);
return !in_array($attribute, $forbiddenList);
}
}

View File

@@ -29,7 +29,9 @@
namespace Espo\Core\Formula;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\FieldProcessing\SpecificFieldLoader;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\ORM\Defs\AttributeParam;
use Espo\Core\Utils\FieldUtil;
use Espo\Entities\EmailAddress;
@@ -53,41 +55,29 @@ class AttributeFetcher
private EntityManager $entityManager,
private FieldUtil $fieldUtil,
private SpecificFieldLoader $specificFieldLoader,
private SystemRestriction $systemRestriction,
) {}
/**
* @throws NotAllowedUsage
*/
public function fetch(Entity $entity, string $attribute, bool $getFetchedAttribute = false): mixed
{
if (str_contains($attribute, '.')) {
$arr = explode('.', $attribute);
$relationName = $arr[0];
$link = $arr[0];
$relatedAttribute = $arr[1] ?? null;
$key = $this->buildKey($entity, $relationName);
if (
!array_key_exists($key, $this->relatedEntitiesCacheMap) &&
$entity->hasRelation($relationName) &&
!in_array(
$entity->getRelationType($relationName),
[Entity::MANY_MANY, Entity::HAS_MANY, Entity::HAS_CHILDREN]
)
) {
$this->relatedEntitiesCacheMap[$key] = $this->entityManager
->getRDBRepository($entity->getEntityType())
->getRelation($entity, $relationName)
->findOne();
if (!$relatedAttribute) {
return null;
}
$relatedEntity = $this->relatedEntitiesCacheMap[$key] ?? null;
return $this->fetchRelated($entity, $link, $relatedAttribute);
}
if (
$relatedEntity instanceof Entity &&
count($arr) > 1
) {
return $this->fetch($relatedEntity, $arr[1]);
}
return null;
if (!$this->systemRestriction->checkAttributeRead($entity->getEntityType(), $attribute)) {
throw new NotAllowedUsage("Cannot read restricted attribute {$entity->getEntityType()}.$attribute.");
}
if ($getFetchedAttribute) {
@@ -184,4 +174,39 @@ class AttributeFetcher
{
return spl_object_hash($entity) . '-' . $link;
}
/**
* @throws NotAllowedUsage
*/
private function fetchRelated(Entity $entity, string $link, string $attribute): mixed
{
$entityType = $entity->getEntityType();
if (!$this->systemRestriction->checkLinkRead($entityType, $link) ) {
throw new NotAllowedUsage("Cannot read restricted link $entityType.$link.");
}
$key = $this->buildKey($entity, $link);
if (
!array_key_exists($key, $this->relatedEntitiesCacheMap) &&
$entity->hasRelation($link) &&
!in_array(
$entity->getRelationType($link),
[Entity::MANY_MANY, Entity::HAS_MANY, Entity::HAS_CHILDREN]
)
) {
$this->relatedEntitiesCacheMap[$key] = $this->entityManager
->getRelation($entity, $link)
->findOne();
}
$relatedEntity = $this->relatedEntitiesCacheMap[$key] ?? null;
if (!$relatedEntity instanceof Entity) {
return null;
}
return $this->fetch($relatedEntity, $attribute);
}
}

View File

@@ -33,6 +33,8 @@ use Espo\Core\Formula\Exceptions\Error;
/**
* A function.
*
* An entity passed to the constructor as of v9.4.0. But it is not an officially guaranteed contract.
*/
interface Func
{

View File

@@ -29,6 +29,7 @@
namespace Espo\Core\Formula;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\Formula\Exceptions\UnknownFunction;
use Espo\Core\Formula\Functions\Base;
use Espo\Core\Formula\Functions\BaseFunction;
@@ -93,7 +94,13 @@ class FunctionFactory
$class->implementsInterface(Func::class) ||
$class->implementsInterface(FuncVariablesAware::class)
) {
return $this->injectableFactory->create($className);
$binding = new BindingContainerBuilder();
if ($entity) {
$binding->bindInstance(Entity::class, $entity);
}
return $this->injectableFactory->createWithBinding($className, $binding->build());
}
$object = $this->injectableFactory->createWith($className, [

View File

@@ -29,8 +29,10 @@
namespace Espo\Core\Formula\Functions;
use Espo\Core\Exceptions\Error;
use Espo\Core\Formula\AttributeFetcher;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use stdClass;
class AttributeType extends Base
{
@@ -49,9 +51,10 @@ class AttributeType extends Base
/**
* @return mixed
* @throws NotAllowedUsage
* @throws Error
*/
public function process(\stdClass $item)
public function process(stdClass $item)
{
if (!property_exists($item, 'value')) {
throw new Error();
@@ -63,6 +66,7 @@ class AttributeType extends Base
/**
* @param string $attribute
* @return mixed
* @throws NotAllowedUsage
* @throws Error
*/
protected function getAttributeValue($attribute)

View File

@@ -62,8 +62,12 @@ abstract class Base
*/
private $variables;
public function __construct(string $name, Processor $processor, ?Entity $entity = null, ?stdClass $variables = null)
{
public function __construct(
string $name,
Processor $processor,
?Entity $entity = null,
?stdClass $variables = null,
) {
$this->name = $name;
$this->processor = $processor;
$this->entity = $entity;

View File

@@ -29,26 +29,50 @@
namespace Espo\Core\Formula\Functions\EntityGroup;
use Espo\Core\Exceptions\Error;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\NotPassedEntity;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\ORM\Entity;
class AddLinkMultipleIdType extends \Espo\Core\Formula\Functions\Base
/**
* @noinspection PhpUnused
*/
class AddLinkMultipleIdType implements Func
{
/**
* @return void
* @throws Error
* @throws \Espo\Core\Formula\Exceptions\Error
*/
public function process(\stdClass $item)
public function __construct(
private SystemRestriction $systemRestriction,
private ?Entity $entity = null,
) {}
public function process(EvaluatedArgumentList $arguments): null
{
if (count($item->value) < 2) {
throw new Error("addLinkMultipleId function: Too few arguments.");
$entity = $this->entity ?? throw new NotPassedEntity();
if (!$entity instanceof CoreEntity) {
throw new Error("Non-core entity.");
}
$link = $this->evaluate($item->value[0]);
$id = $this->evaluate($item->value[1]);
if (count($arguments) < 2) {
throw TooFewArguments::create(2);
}
$link = $arguments[0];
$id = $arguments[1];
if (!is_string($link)) {
throw new Error();
throw BadArgumentType::create(1, 'string');
}
$entityType = $entity->getEntityType();
if (!$this->systemRestriction->checkFieldWrite($entityType, $link)) {
throw new NotAllowedUsage("Cannot write restricted field $entityType.$link.");
}
if (is_array($id)) {
@@ -56,17 +80,21 @@ class AddLinkMultipleIdType extends \Espo\Core\Formula\Functions\Base
foreach ($idList as $id) {
if (!is_string($id)) {
throw new Error();
throw BadArgumentType::create(2, 'string[]');
}
$this->getEntity()->addLinkMultipleId($link, $id);
}
} else {
if (!is_string($id)) {
return;
$entity->addLinkMultipleId($link, $id);
}
$this->getEntity()->addLinkMultipleId($link, $id);
return null;
}
if (!is_string($id)) {
return null;
}
$entity->addLinkMultipleId($link, $id);
return null;
}
}

View File

@@ -29,8 +29,15 @@
namespace Espo\Core\Formula\Functions\EntityGroup;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
class AttributeFetchedType extends AttributeType
{
/**
* @throws NotAllowedUsage
* @throws Error
*/
protected function getAttributeValue($attribute)
{
return $this->attributeFetcher->fetch($this->getEntity(), $attribute, true);

View File

@@ -29,18 +29,23 @@
namespace Espo\Core\Formula\Functions\EntityGroup;
use Espo\Core\Exceptions\Error;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\TooFewArguments;
class AttributeType extends \Espo\Core\Formula\Functions\AttributeType
{
public function process(\stdClass $item)
{
if (count($item->value) < 1) {
throw new Error("attribute function: Too few arguments.");
throw TooFewArguments::create(1);
}
$attribute = $this->evaluate($item->value[0]);
if (!is_string($attribute)) {
throw BadArgumentType::create(1, 'string');
}
return $this->getAttributeValue($attribute);
}
}

View File

@@ -29,31 +29,52 @@
namespace Espo\Core\Formula\Functions\EntityGroup;
use Espo\Core\Formula\ArgumentList;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\NotPassedEntity;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Formula\Func;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\ORM\Entity;
/**
* @noinspection PhpUnused
*/
class ClearAttributeType extends BaseFunction
class ClearAttributeType implements Func
{
public function process(ArgumentList $args)
public function __construct(
private SystemRestriction $systemRestriction,
private ?Entity $entity = null,
) {}
public function process(EvaluatedArgumentList $arguments): null
{
if (count($args) < 1) {
$entity = $this->entity ?? throw new NotPassedEntity();
if (!$entity instanceof CoreEntity) {
throw new Error("Non-core entity.");
}
if (count($arguments) < 1) {
throw TooFewArguments::create(1);
}
$args = $this->evaluate($args);
$attribute = $args[0];
$attribute = $arguments[0];
if (!is_string($attribute)) {
throw BadArgumentType::create(1, 'string');
}
$this->getEntity()->clear($attribute);
$entityType = $entity->getEntityType();
if (!$this->systemRestriction->checkAttributeWrite($entityType, $attribute)) {
throw new NotAllowedUsage("Cannot write restricted attribute $entityType.$attribute.");
}
$entity->clear($attribute);
return null;
}

View File

@@ -29,67 +29,78 @@
namespace Espo\Core\Formula\Functions\EntityGroup;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Functions\Base;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\NotPassedEntity;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\Formula\Functions\RecordGroup\Util\FindQueryUtil;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\Core\Di;
use stdClass;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
/**
* @noinspection PhpUnused
*/
class CountRelatedType extends Base implements
Di\EntityManagerAware,
Di\InjectableFactoryAware,
Di\UserAware
class CountRelatedType implements Func
{
use Di\EntityManagerSetter;
use Di\InjectableFactorySetter;
use Di\UserSetter;
public function __construct(
private SystemRestriction $systemRestriction,
private EntityManager $entityManager,
private SelectBuilderFactory $selectBuilderFactory,
private FindQueryUtil $findQueryUtil,
private ?Entity $entity = null,
) {}
/**
* @return int
* @throws Error
*/
public function process(stdClass $item)
public function process(EvaluatedArgumentList $arguments): int
{
if (count($item->value) < 1) {
throw new Error("countRelated: roo few arguments.");
$entity = $this->entity ?? throw new NotPassedEntity();
if (!$entity instanceof CoreEntity) {
throw new Error("Non-core entity.");
}
$link = $this->evaluate($item->value[0]);
if (count($arguments) < 1) {
throw TooFewArguments::create(1);
}
if (empty($link)) {
throw new Error("countRelated: no link passed.");
$link = $arguments[0];
if (!is_string($link)) {
throw BadArgumentType::create(1, 'string');
}
$entityType = $entity->getEntityType();
if (!$this->systemRestriction->checkLinkRead($entityType, $link)) {
throw new NotAllowedUsage("Cannot read restricted field $entityType.$link.");
}
$filter = null;
if (count($item->value) > 1) {
$filter = $this->evaluate($item->value[1]);
if (count($arguments) > 1) {
$filter = $arguments[1];
}
$entity = $this->getEntity();
$entityManager = $this->entityManager;
$foreignEntityType = $entity->getRelationParam($link, RelationParam::ENTITY);
if (empty($foreignEntityType)) {
throw new Error();
throw new Error("Not supported link $link.");
}
$builder = $this->injectableFactory->create(SelectBuilderFactory::class)
$builder = $this->selectBuilderFactory
->create()
->forUser($this->user)
->from($foreignEntityType);
if ($filter) {
(new FindQueryUtil())->applyFilter($builder, $filter, 2);
$this->findQueryUtil->applyFilter($builder, $filter, 2);
}
try {
@@ -98,7 +109,8 @@ class CountRelatedType extends Base implements
throw new Error($e->getMessage());
}
return $entityManager->getRDBRepository($entity->getEntityType())
return $this->entityManager
->getRDBRepository($entity->getEntityType())
->getRelation($entity, $link)
->clone($query)
->count();

View File

@@ -29,44 +29,67 @@
namespace Espo\Core\Formula\Functions\EntityGroup;
use Espo\Core\Exceptions\Error;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\NotPassedEntity;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\Core\Di;
class GetLinkColumnType extends \Espo\Core\Formula\Functions\Base implements
Di\EntityManagerAware
/**
* @noinspection PhpUnused
*/
class GetLinkColumnType implements Func
{
use Di\EntityManagerSetter;
public function __construct(
private SystemRestriction $systemRestriction,
private EntityManager $entityManager,
private ?Entity $entity = null,
) {}
/**
* @var EntityManager
*/
protected $entityManager;
/**
* @return mixed
* @throws Error
* @throws \Espo\Core\Formula\Exceptions\Error
*/
public function process(\stdClass $item)
public function process(EvaluatedArgumentList $arguments): mixed
{
$args = $item->value ?? [];
$entity = $this->entity ?? throw new NotPassedEntity();
if (count($args) < 3) {
throw new Error("Formula: entity\\isRelated: no argument.");
if (!$entity instanceof CoreEntity) {
throw new Error("Non-core entity.");
}
$link = $this->evaluate($args[0]);
$id = $this->evaluate($args[1]);
$column = $this->evaluate($args[2]);
if (count($arguments) < 3) {
throw TooFewArguments::create(3);
}
$link = $arguments[0];
$id = $arguments[1];
$column = $arguments[2];
if (!is_string($link)) {
throw BadArgumentType::create(1, 'string');
}
if (!is_string($id)) {
throw BadArgumentType::create(2, 'string');
}
if (!is_string($column)) {
throw BadArgumentType::create(3, 'string');
}
$entityType = $entity->getEntityType();
if (!$this->systemRestriction->checkFieldRead($entityType, $link)) {
throw new NotAllowedUsage("Cannot read restricted field $entityType.$link.");
}
$entityType = $this->getEntity()->getEntityType();
$repository = $this->entityManager->getRDBRepository($entityType);
return $repository
->getRelation($this->getEntity(), $link)
->getRelation($entity, $link)
->getColumnById($id, $column);
}
}

View File

@@ -29,32 +29,56 @@
namespace Espo\Core\Formula\Functions\EntityGroup;
use Espo\Core\Exceptions\Error;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\NotPassedEntity;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\ORM\Entity;
class HasLinkMultipleIdType extends \Espo\Core\Formula\Functions\Base
/**
* @noinspection PhpUnused
*/
class HasLinkMultipleIdType implements Func
{
/**
* @return bool
* @throws Error
* @throws \Espo\Core\Formula\Exceptions\Error
*/
public function process(\stdClass $item)
public function __construct(
private SystemRestriction $systemRestriction,
private ?Entity $entity = null,
) {}
public function process(EvaluatedArgumentList $arguments): bool
{
if (count($item->value) < 2) {
throw new Error("hasLinkMultipleId: too few arguments.");
$entity = $this->entity ?? throw new NotPassedEntity();
if (!$entity instanceof CoreEntity) {
throw new Error("Non-core entity.");
}
$link = $this->evaluate($item->value[0]);
$id = $this->evaluate($item->value[1]);
if (count($arguments) < 2) {
throw TooFewArguments::create(2);
}
$link = $arguments[0];
$id = $arguments[1];
if (!is_string($link)) {
throw new Error();
throw BadArgumentType::create(1, 'string');
}
if (!is_string($id)) {
throw new Error();
throw BadArgumentType::create(2, 'string');
}
return $this->getEntity()->hasLinkMultipleId($link, $id);
$entityType = $entity->getEntityType();
if (!$this->systemRestriction->checkFieldRead($entityType, $link)) {
throw new NotAllowedUsage("Cannot read restricted field $entityType.$link.");
}
return $entity->hasLinkMultipleId($link, $id);
}
}

View File

@@ -29,23 +29,29 @@
namespace Espo\Core\Formula\Functions\EntityGroup;
use Espo\Core\Exceptions\Error;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Functions\Base;
class IsAttributeChangedType extends \Espo\Core\Formula\Functions\Base
class IsAttributeChangedType extends Base
{
/**
* @return bool
* @throws Error
* @throws \Espo\Core\Formula\Exceptions\Error
*/
public function process(\stdClass $item)
{
if (count($item->value) < 1) {
throw new Error("isAttributeChanged: too few arguments.");
throw TooFewArguments::create(1);
}
$attribute = $this->evaluate($item->value[0]);
if (!is_string($attribute)) {
throw BadArgumentType::create(1, 'string');
}
return $this->check($attribute);
}

View File

@@ -29,12 +29,14 @@
namespace Espo\Core\Formula\Functions\EntityGroup;
use Espo\Core\Formula\Exceptions\Error;
class IsAttributeNotChangedType extends IsAttributeChangedType
{
/**
* @param string $attribute
* @return bool
* @throws \Espo\Core\Exceptions\Error
* @throws Error
*/
protected function check($attribute)
{

View File

@@ -29,11 +29,13 @@
namespace Espo\Core\Formula\Functions\EntityGroup;
use Espo\Core\Formula\Exceptions\Error;
class IsNewType extends \Espo\Core\Formula\Functions\Base
{
/**
* @return bool
* @throws \Espo\Core\Exceptions\Error
* @throws Error
*/
public function process(\stdClass $item)
{

View File

@@ -29,32 +29,60 @@
namespace Espo\Core\Formula\Functions\EntityGroup;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Functions\Base;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\NotPassedEntity;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\Core\Di;
class IsRelatedType extends Base implements
Di\EntityManagerAware
/**
* @noinspection PhpUnused
*/
class IsRelatedType implements Func
{
use Di\EntityManagerSetter;
public function __construct(
private SystemRestriction $systemRestriction,
private EntityManager $entityManager,
private ?Entity $entity = null,
) {}
/**
* @return bool
* @throws Error
*/
public function process(\stdClass $item)
public function process(EvaluatedArgumentList $arguments): bool
{
if (count($item->value) < 2) {
throw new Error("isRelated: roo few arguments.");
$entity = $this->entity ?? throw new NotPassedEntity();
if (!$entity instanceof CoreEntity) {
throw new Error("Non-core entity.");
}
$link = $this->evaluate($item->value[0]);
$id = $this->evaluate($item->value[1]);
if (count($arguments) < 2) {
throw TooFewArguments::create(1);
}
$link = $arguments[0];
$id = $arguments[1];
if (!is_string($link)) {
throw BadArgumentType::create(1, 'string');
}
if (!is_string($id)) {
throw BadArgumentType::create(2, 'string');
}
$entityType = $entity->getEntityType();
if (!$this->systemRestriction->checkLinkRead($entityType, $link)) {
throw new NotAllowedUsage("Cannot read restricted field $entityType.$link.");
}
return $this->entityManager
->getRDBRepository($this->getEntity()->getEntityType())
->getRelation($this->getEntity(), $link)
->getRelation($entity, $link)
->isRelatedById($id);
}
}

View File

@@ -29,32 +29,58 @@
namespace Espo\Core\Formula\Functions\EntityGroup;
use Espo\Core\Exceptions\Error;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\NotPassedEntity;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\ORM\Entity;
class RemoveLinkMultipleIdType extends \Espo\Core\Formula\Functions\Base
/**
* @noinspection PhpUnused
*/
class RemoveLinkMultipleIdType implements Func
{
/**
* @return void
* @throws \Espo\Core\Formula\Exceptions\Error
* @throws Error
*/
public function process(\stdClass $item)
public function __construct(
private SystemRestriction $systemRestriction,
private ?Entity $entity = null,
) {}
public function process(EvaluatedArgumentList $arguments): null
{
if (count($item->value) < 2) {
throw new Error("removeLinkMultipleId: roo few arguments.");
$entity = $this->entity ?? throw new NotPassedEntity();
if (!$entity instanceof CoreEntity) {
throw new Error("Non-core entity.");
}
$link = $this->evaluate($item->value[0]);
$id = $this->evaluate($item->value[1]);
if (count($arguments) < 2) {
throw TooFewArguments::create(2);
}
$link = $arguments[0];
$id = $arguments[1];
if (!is_string($link)) {
throw new Error();
throw BadArgumentType::create(1, 'string');
}
if (!is_string($id)) {
throw new Error();
throw BadArgumentType::create(2, 'string');
}
$this->getEntity()->removeLinkMultipleId($link, $id);
$entityType = $entity->getEntityType();
if (!$this->systemRestriction->checkFieldWrite($entityType, $link)) {
throw new NotAllowedUsage("Cannot write restricted field $entityType.$link.");
}
$entity->removeLinkMultipleId($link, $id);
return null;
}
}

View File

@@ -29,30 +29,64 @@
namespace Espo\Core\Formula\Functions\EntityGroup;
use Espo\Core\Formula\ArgumentList;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\ORM\Entity;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\NotPassedEntity;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\ORM\Entity;
class SetLinkMultipleColumnType extends BaseFunction
/**
* @noinspection PhpUnused
*/
class SetLinkMultipleColumnType implements Func
{
public function process(ArgumentList $args)
public function __construct(
private SystemRestriction $systemRestriction,
private ?Entity $entity = null,
) {}
public function process(EvaluatedArgumentList $arguments): null
{
if (count($args) < 4) {
$this->throwTooFewArguments(4);
$entity = $this->entity ?? throw new NotPassedEntity();
if (!$entity instanceof CoreEntity) {
throw new Error("Non-core entity.");
}
$link = $this->evaluate($args[0]);
$id = $this->evaluate($args[1]);
$column = $this->evaluate($args[2]);
$value = $this->evaluate($args[3]);
if (count($arguments) < 4) {
throw TooFewArguments::create(4);
}
$entity = $this->getEntity();
$link = $arguments[0];
$id = $arguments[1];
$column = $arguments[2];
$value = $arguments[3];
if (!$entity instanceof Entity) {
throw new Error();
if (!is_string($link)) {
throw BadArgumentType::create(1, 'string');
}
if (!is_string($id)) {
throw BadArgumentType::create(2, 'string');
}
if (!is_string($column)) {
throw BadArgumentType::create(3, 'string');
}
$entityType = $entity->getEntityType();
if (!$this->systemRestriction->checkFieldWrite($entityType, $link)) {
throw new NotAllowedUsage("Cannot write restricted field $entityType.$link.");
}
$entity->setLinkMultipleColumn($link, $column, $id, $value);
return null;
}
}

View File

@@ -29,82 +29,96 @@
namespace Espo\Core\Formula\Functions\EntityGroup;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Di;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Formula\Functions\Base;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\NotPassedEntity;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\Formula\Functions\RecordGroup\Util\FindQueryUtil;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\ORM\Name\Attribute;
use stdClass;
use PDO;
/**
* @noinspection PhpUnused
*/
class SumRelatedType extends Base implements
Di\EntityManagerAware,
Di\InjectableFactoryAware,
Di\UserAware
class SumRelatedType implements Func
{
use Di\EntityManagerSetter;
use Di\InjectableFactorySetter;
use Di\UserSetter;
public function __construct(
private SystemRestriction $systemRestriction,
private EntityManager $entityManager,
private SelectBuilderFactory $selectBuilderFactory,
private FindQueryUtil $findQueryUtil,
private ?Entity $entity = null,
) {}
/**
* @return float
* @throws Error
*/
public function process(stdClass $item)
public function process(EvaluatedArgumentList $arguments): int|float
{
if (count($item->value) < 2) {
throw new Error("sumRelated: Too few arguments.");
$entity = $this->entity ?? throw new NotPassedEntity();
if (!$entity instanceof CoreEntity) {
throw new Error("Non-core entity.");
}
$link = $this->evaluate($item->value[0]);
if (empty($link)) {
throw new Error("No link passed to sumRelated function.");
if (count($arguments) < 2) {
throw TooFewArguments::create(1);
}
$field = $this->evaluate($item->value[1]);
$link = $arguments[0];
$field = $arguments[1];
if (empty($field)) {
throw new Error("No field passed to sumRelated function.");
if (!is_string($link)) {
throw BadArgumentType::create(1, 'string');
}
if (!is_string($field)) {
throw BadArgumentType::create(2, 'string');
}
$filter = null;
if (count($item->value) > 2) {
$filter = $this->evaluate($item->value[2]);
if (count($arguments) > 2) {
$filter = $arguments[2];
}
$entity = $this->getEntity();
$entityType = $entity->getEntityType();
$entityManager = $this->entityManager;
if (!$this->systemRestriction->checkLinkRead($entityType, $link)) {
throw new NotAllowedUsage("Cannot read restricted field $entityType.$link.");
}
$foreignEntityType = $entity->getRelationParam($link, RelationParam::ENTITY);
if (empty($foreignEntityType)) {
throw new Error();
if (!$foreignEntityType) {
throw new Error("Not supported link '$link'.");
}
if (!$this->systemRestriction->checkLinkRead($foreignEntityType, $field)) {
throw new NotAllowedUsage("Cannot read restricted field $foreignEntityType.$field.");
}
$foreignLink = $entity->getRelationParam($link, RelationParam::FOREIGN);
$foreignLinkAlias = $foreignLink . 'SumRelated';
if (empty($foreignLink)) {
throw new Error("No foreign link for link {$link}.");
throw new Error("No foreign link for link $link.");
}
$builder = $this->injectableFactory->create(SelectBuilderFactory::class)
$builder = $this->selectBuilderFactory
->create()
->forUser($this->user)
->from($foreignEntityType);
if ($filter) {
(new FindQueryUtil())->applyFilter($builder, $filter, 3);
$this->findQueryUtil->applyFilter($builder, $filter, 3);
}
try {
@@ -156,7 +170,7 @@ class SumRelatedType extends Base implements
$queryBuilder->group($foreignLinkAlias . '.id');
$sth = $entityManager->getQueryExecutor()->execute($queryBuilder->build());
$sth = $this->entityManager->getQueryExecutor()->execute($queryBuilder->build());
$rowList = $sth->fetchAll(PDO::FETCH_ASSOC);

View File

@@ -29,9 +29,11 @@
namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Di;
use stdClass;
class AttributeType extends \Espo\Core\Formula\Functions\AttributeType implements
Di\EntityManagerAware
@@ -41,7 +43,7 @@ class AttributeType extends \Espo\Core\Formula\Functions\AttributeType implement
/**
* @throws Error
*/
public function process(\stdClass $item)
public function process(stdClass $item)
{
if (count($item->value) < 3) {
throw new Error("record\\attribute: too few arguments.");
@@ -51,16 +53,16 @@ class AttributeType extends \Espo\Core\Formula\Functions\AttributeType implement
$id = $this->evaluate($item->value[1]);
$attribute = $this->evaluate($item->value[2]);
if (!$entityType) {
throw new Error("Formula record\\attribute: Empty entityType.");
if (!is_string($entityType)) {
throw BadArgumentType::create(1, 'string');
}
if (!$id) {
return null;
if (!is_string($id)) {
throw BadArgumentType::create(2, 'string');
}
if (!$attribute) {
throw new Error("Formula record\\attribute: Empty attribute.");
if (!is_string($attribute)) {
throw BadArgumentType::create(3, 'string');
}
$entity = $this->entityManager->getEntityById($entityType, $id);

View File

@@ -31,48 +31,52 @@ namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Formula\ArgumentList;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\Formula\Functions\RecordGroup\Util\FindQueryUtil;
use Espo\Core\Di;
use Espo\Core\Select\Primary\Filters\All;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\ORM\EntityManager;
/**
* @noinspection PhpUnused
*/
class CountType extends BaseFunction implements
Di\EntityManagerAware,
Di\InjectableFactoryAware,
Di\UserAware
class CountType implements Func
{
use Di\EntityManagerSetter;
use Di\InjectableFactorySetter;
use Di\UserSetter;
public function __construct(
private EntityManager $entityManager,
private SelectBuilderFactory $selectBuilderFactory,
private FindQueryUtil $findQueryUtil,
) {}
public function process(ArgumentList $args)
public function process(EvaluatedArgumentList $arguments): int
{
if (count($args) < 1) {
$this->throwTooFewArguments(1);
if (count($arguments) < 1) {
throw TooFewArguments::create(1);
}
$entityType = $this->evaluate($args[0]);
$entityType = $arguments[0];
if (count($args) < 3) {
if (!is_string($entityType)) {
throw BadArgumentType::create(1, 'string');
}
if (count($arguments) < 3) {
$filter = null;
if (count($args) === 2) {
$filter = $this->evaluate($args[1]);
if (count($arguments) === 2) {
$filter = $arguments[1];
}
$builder = $this->injectableFactory->create(SelectBuilderFactory::class)
$builder = $this->selectBuilderFactory
->create()
->forUser($this->user)
->withPrimaryFilter(All::NAME)
->from($entityType);
(new FindQueryUtil())->applyFilter($builder, $filter, 2);
$this->findQueryUtil->applyFilter($builder, $filter, 2);
try {
return $this->entityManager
@@ -80,7 +84,7 @@ class CountType extends BaseFunction implements
->clone($builder->build())
->count();
} catch (BadRequest|Forbidden $e) {
throw new Error($e->getMessage(), 0, $e);
throw new NotAllowedUsage($e->getMessage(), 0, $e);
}
}
@@ -88,9 +92,11 @@ class CountType extends BaseFunction implements
$i = 1;
while ($i < count($args) - 1) {
$key = $this->evaluate($args[$i]);
$value = $this->evaluate($args[$i + 1]);
while ($i < count($arguments) - 1) {
$key = $arguments[$i];
$value = $arguments[$i + 1];
$this->findQueryUtil->assertWhereClauseKeyValid($entityType, $key);
$whereClause[] = [$key => $value];

View File

@@ -29,46 +29,52 @@
namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Formula\ArgumentList;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Di;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\Formula\Utils\EntityUtil;
use RuntimeException;
use Espo\ORM\EntityManager;
use stdClass;
/**
* @noinspection PhpUnused
*/
class CreateType extends BaseFunction implements
Di\EntityManagerAware
class CreateType implements Func
{
use Di\EntityManagerSetter;
public function __construct(
private EntityManager $entityManager,
private EntityUtil $entityUtil,
) {}
public function process(ArgumentList $args)
public function process(EvaluatedArgumentList $arguments): ?string
{
if (count($args) < 1) {
$this->throwTooFewArguments(1);
if (count($arguments) < 1) {
throw TooFewArguments::create(1);
}
$args = $this->evaluate($args);
if (!is_array($args)) {
throw new RuntimeException();
}
$entityType = $args[0];
$entityType = $arguments[0];
if (!is_string($entityType)) {
$this->throwBadArgumentType(1, 'string');
throw BadArgumentType::create(1, 'string');
}
$data = $this->getData($args, $entityType);
$data = $this->getData($arguments, $entityType);
$notAllowedAttributes = array_intersect(
array_keys($data),
$this->entityUtil->getWriteRestrictedAttributeList($entityType),
);
if ($notAllowedAttributes) {
throw new NotAllowedUsage("Cannot write $entityType.$notAllowedAttributes[0].");
}
$entity = $this->entityManager->getNewEntity($entityType);
$entity->setMultiple($data);
EntityUtil::checkUpdateAccess($entity);
$this->entityUtil->assertUpdateAccess($entity);
$this->entityManager->saveEntity($entity);
@@ -76,11 +82,10 @@ class CreateType extends BaseFunction implements
}
/**
* @param array<int, mixed> $args
* @return array<string, mixed>
* @throws BadArgumentType
*/
private function getData(array $args, mixed $entityType): array
private function getData(EvaluatedArgumentList $args, mixed $entityType): array
{
if (count($args) >= 2 && $args[1] instanceof stdClass) {
return get_object_vars($args[1]);
@@ -94,7 +99,7 @@ class CreateType extends BaseFunction implements
$attribute = $args[$i];
if (!is_string($entityType)) {
$this->throwBadArgumentType($i + 1, 'string');
throw BadArgumentType::create($i + 1, 'string');
}
/** @var string $attribute */

View File

@@ -36,11 +36,17 @@ use Espo\Core\Formula\Func;
use Espo\Core\Formula\Utils\EntityUtil;
use Espo\ORM\EntityManager;
/**
* @noinspection PhpUnused
*/
class DeleteType implements Func
{
public function __construct(private EntityManager $entityManager) {}
public function __construct(
private EntityManager $entityManager,
private EntityUtil $entityUtil,
) {}
public function process(EvaluatedArgumentList $arguments): mixed
public function process(EvaluatedArgumentList $arguments): null
{
if (count($arguments) < 2) {
throw TooFewArguments::create(2);
@@ -63,7 +69,7 @@ class DeleteType implements Func
return null;
}
EntityUtil::checkRemoveAccess($entity);
$this->entityUtil->assertRemoveAccess($entity);
$this->entityManager->removeEntity($entity);

View File

@@ -31,48 +31,53 @@ namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Formula\ArgumentList;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Di;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\Formula\Functions\RecordGroup\Util\FindQueryUtil;
use Espo\Core\Select\Primary\Filters\All;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\ORM\EntityManager;
use Espo\ORM\Name\Attribute;
/**
* @noinspection PhpUnused
*/
class ExistsType extends BaseFunction implements
Di\EntityManagerAware,
Di\InjectableFactoryAware,
Di\UserAware
class ExistsType implements Func
{
use Di\EntityManagerSetter;
use Di\InjectableFactorySetter;
use Di\UserSetter;
public function __construct(
private EntityManager $entityManager,
private SelectBuilderFactory $selectBuilderFactory,
private FindQueryUtil $findQueryUtil,
) {}
public function process(ArgumentList $args)
public function process(EvaluatedArgumentList $arguments): bool
{
if (count($args) < 1) {
$this->throwTooFewArguments(1);
if (count($arguments) < 1) {
throw TooFewArguments::create(1);
}
$entityType = $this->evaluate($args[0]);
$entityType = $arguments[0];
if (count($args) <= 2) {
if (!is_string($entityType)) {
throw BadArgumentType::create(1, 'string');
}
if (count($arguments) <= 2) {
$filter = null;
if (count($args) === 2) {
$filter = $this->evaluate($args[1]);
if (count($arguments) === 2) {
$filter = $arguments[1];
}
$builder = $this->injectableFactory->create(SelectBuilderFactory::class)
$builder = $this->selectBuilderFactory
->create()
->forUser($this->user)
->withPrimaryFilter(All::NAME)
->from($entityType);
(new FindQueryUtil())->applyFilter($builder, $filter, 2);
$this->findQueryUtil->applyFilter($builder, $filter, 2);
try {
return (bool) $this->entityManager
@@ -80,7 +85,7 @@ class ExistsType extends BaseFunction implements
->clone($builder->build())
->findOne();
} catch (BadRequest|Forbidden $e) {
throw new Error($e->getMessage(), 0, $e);
throw new NotAllowedUsage($e->getMessage(), 0, $e);
}
}
@@ -88,9 +93,11 @@ class ExistsType extends BaseFunction implements
$i = 1;
while ($i < count($args) - 1) {
$key = $this->evaluate($args[$i]);
$value = $this->evaluate($args[$i + 1]);
while ($i < count($arguments) - 1) {
$key = $arguments[$i];
$value = $arguments[$i + 1];
$this->findQueryUtil->assertWhereClauseKeyValid($entityType, $key);
$whereClause[] = [$key => $value];
@@ -99,6 +106,7 @@ class ExistsType extends BaseFunction implements
return (bool) $this->entityManager
->getRDBRepository($entityType)
->select([Attribute::ID])
->where($whereClause)
->findOne();
}

View File

@@ -29,6 +29,7 @@
namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\TooFewArguments;
@@ -41,7 +42,10 @@ use stdClass;
class FetchType implements Func
{
public function __construct(private EntityManager $entityManager) {}
public function __construct(
private EntityManager $entityManager,
private SystemRestriction $systemRestriction,
) {}
public function process(EvaluatedArgumentList $arguments): ?stdClass
{
@@ -68,6 +72,10 @@ class FetchType implements Func
$this->load($entity);
foreach ($this->systemRestriction->getReadRestrictedAttributeList($entityType) as $attribute) {
$entity->clear($attribute);
}
return $entity->getValueMap();
}

View File

@@ -33,8 +33,7 @@ use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\BadArgumentValue;
use Espo\Core\Formula\Exceptions\Error as FormulaError;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\Formula\Functions\RecordGroup\Util\FindQueryUtil;
@@ -43,13 +42,16 @@ use Espo\Core\Select\SelectBuilderFactory;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Query\Part\Order;
/**
* @noinspection PhpUnused
*/
class FindManyType implements Func
{
public function __construct(
private EntityManager $entityManager,
private SelectBuilderFactory $selectBuilderFactory
private SelectBuilderFactory $selectBuilderFactory,
private FindQueryUtil $findQueryUtil,
) {}
/**
@@ -58,14 +60,14 @@ class FindManyType implements Func
*/
public function process(EvaluatedArgumentList $arguments): array
{
if (count($arguments) < 4) {
throw TooFewArguments::create(4);
if (count($arguments) < 2) {
throw TooFewArguments::create(2);
}
$entityType = $arguments[0];
$limit = $arguments[1];
$orderBy = $arguments[2];
$order = $arguments[3] ?? Order::ASC;
$orderBy = $arguments[2] ?? null;
$order = $arguments[3] ?? null;
if (!is_string($entityType)) {
throw BadArgumentType::create(1, 'string');
@@ -79,16 +81,8 @@ class FindManyType implements Func
throw BadArgumentType::create(3, 'string|null');
}
if (!is_bool($order) && !is_string($order)) {
throw BadArgumentType::create(4, 'string|bool');
}
if (is_string($order)) {
$order = strtoupper($order);
if ($order !== Order::ASC && $order !== Order::DESC) {
throw BadArgumentValue::create(4, 'Bad order value.');
}
if ($order !== null && !is_bool($order) && !is_string($order)) {
throw BadArgumentType::create(4, 'string|bool|null');
}
$builder = $this->selectBuilderFactory
@@ -96,6 +90,8 @@ class FindManyType implements Func
->withPrimaryFilter(All::NAME)
->from($entityType);
$this->findQueryUtil->applyOrder($builder, $orderBy, $order, 4);
$whereClause = [];
if (count($arguments) <= 5) {
@@ -105,7 +101,7 @@ class FindManyType implements Func
$filter = $arguments[4];
}
(new FindQueryUtil())->applyFilter($builder, $filter, 5);
$this->findQueryUtil->applyFilter($builder, $filter, 5);
} else {
$i = 4;
@@ -113,6 +109,8 @@ class FindManyType implements Func
$key = $arguments[$i];
$value = $arguments[$i + 1];
$this->findQueryUtil->assertWhereClauseKeyValid($entityType, $key);
$whereClause[] = [$key => $value];
$i = $i + 2;
@@ -122,17 +120,13 @@ class FindManyType implements Func
try {
$queryBuilder = $builder->buildQueryBuilder();
} catch (BadRequest|Forbidden $e) {
throw new FormulaError($e->getMessage(), $e->getCode(), $e);
throw new NotAllowedUsage($e->getMessage(), $e->getCode(), $e);
}
if (!empty($whereClause)) {
$queryBuilder->where($whereClause);
}
if ($orderBy) {
$queryBuilder->order($orderBy, $order);
}
$queryBuilder
->select([Attribute::ID])
->limit(0, $limit);

View File

@@ -31,60 +31,75 @@ namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Formula\ArgumentList;
use Espo\Core\Formula\Exceptions\Error as FormulaError;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Di;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\Formula\Functions\RecordGroup\Util\FindQueryUtil;
use Espo\Core\Select\Primary\Filters\All;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\ORM\EntityManager;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Query\Part\Order;
/**
* @noinspection PhpUnused
*/
class FindOneType extends BaseFunction implements
Di\EntityManagerAware,
Di\InjectableFactoryAware,
Di\UserAware
class FindOneType implements Func
{
use Di\EntityManagerSetter;
use Di\InjectableFactorySetter;
use Di\UserSetter;
public function __construct(
private EntityManager $entityManager,
private SelectBuilderFactory $selectBuilderFactory,
private FindQueryUtil $findQueryUtil,
) {}
public function process(ArgumentList $args)
public function process(EvaluatedArgumentList $arguments): ?string
{
if (count($args) < 3) {
$this->throwTooFewArguments(3);
if (count($arguments) < 1) {
throw TooFewArguments::create(1);
}
$entityType = $this->evaluate($args[0]);
$orderBy = $this->evaluate($args[1]);
$order = $this->evaluate($args[2]) ?? Order::ASC;
$entityType = $arguments[0];
$orderBy = $arguments[1] ?? null;
$order = $arguments[2] ?? null;
$builder = $this->injectableFactory->create(SelectBuilderFactory::class)
if (!is_string($entityType)) {
throw BadArgumentType::create(1, 'string');
}
if ($orderBy !== null && !is_string($orderBy)) {
throw BadArgumentType::create(2, 'string|null');
}
if ($order !== null && !is_bool($order) && !is_string($order)) {
throw BadArgumentType::create(3, 'string|bool|null');
}
$builder = $this->selectBuilderFactory
->create()
->forUser($this->user)
->withPrimaryFilter(All::NAME)
->from($entityType);
$this->findQueryUtil->applyOrder($builder, $orderBy, $order, 3);
$whereClause = [];
if (count($args) <= 4) {
if (count($arguments) <= 4) {
$filter = null;
if (count($args) === 4) {
$filter = $this->evaluate($args[3]);
if (count($arguments) === 4) {
$filter = $arguments[3];
}
(new FindQueryUtil())->applyFilter($builder, $filter, 4);
$this->findQueryUtil->applyFilter($builder, $filter, 4);
} else {
$i = 3;
while ($i < count($args) - 1) {
$key = $this->evaluate($args[$i]);
$value = $this->evaluate($args[$i + 1]);
while ($i < count($arguments) - 1) {
$key = $arguments[$i];
$value = $arguments[$i + 1];
$this->findQueryUtil->assertWhereClauseKeyValid($entityType, $key);
$whereClause[] = [$key => $value];
@@ -95,17 +110,13 @@ class FindOneType extends BaseFunction implements
try {
$queryBuilder = $builder->buildQueryBuilder();
} catch (BadRequest|Forbidden $e) {
throw new FormulaError($e->getMessage(), $e->getCode(), $e);
throw new NotAllowedUsage($e->getMessage(), $e->getCode(), $e);
}
if (!empty($whereClause)) {
$queryBuilder->where($whereClause);
}
if ($orderBy) {
$queryBuilder->order($orderBy, $order);
}
$queryBuilder->select([Attribute::ID]);
$entity = $this->entityManager
@@ -113,10 +124,6 @@ class FindOneType extends BaseFunction implements
->clone($queryBuilder->build())
->findOne();
if ($entity) {
return $entity->getId();
}
return null;
return $entity?->getId();
}
}

View File

@@ -29,117 +29,105 @@
namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\ExecutionException;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\Formula\Functions\RecordGroup\Util\FindQueryUtil;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\Formula\ArgumentList;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Di;
use Espo\Core\Select\Helpers\RandomStringGenerator;
use Espo\Core\Select\Primary\Filters\All;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\EntityManager;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Type\RelationType;
/**
* @noinspection PhpUnused
*/
class FindRelatedManyType extends BaseFunction implements
Di\EntityManagerAware,
Di\MetadataAware,
Di\InjectableFactoryAware,
Di\UserAware
class FindRelatedManyType implements Func
{
use Di\EntityManagerSetter;
use Di\MetadataSetter;
use Di\InjectableFactorySetter;
use Di\UserSetter;
public function __construct(
private EntityManager $entityManager,
private SelectBuilderFactory $selectBuilderFactory,
private FindQueryUtil $findQueryUtil,
private RandomStringGenerator $randomStringGenerator,
private SystemRestriction $systemRestriction,
) {}
/**
* @return string[]
* @throws Error
* @throws TooFewArguments
* @throws BadArgumentType
* @throws ExecutionException
*/
public function process(ArgumentList $args)
public function process(EvaluatedArgumentList $arguments): array
{
$args = $this->evaluate($args);
if (count($args) < 4) {
$this->throwTooFewArguments(4);
if (count($arguments) < 4) {
throw TooFewArguments::create(4);
}
$entityManager = $this->entityManager;
$entityType = $args[0];
$id = $args[1];
$link = $args[2];
$limit = $args[3];
$entityType = $arguments[0];
$id = $arguments[1];
$link = $arguments[2];
$limit = $arguments[3];
$orderBy = null;
$order = null;
if (count($args) > 4) {
$orderBy = $args[4];
if (count($arguments) > 4) {
$orderBy = $arguments[4];
}
if (count($args) > 5) {
$order = $args[5];
if (count($arguments) > 5) {
$order = $arguments[5];
}
if (!$entityType || !is_string($entityType)) {
$this->throwBadArgumentType(1, 'string');
}
if (!$id) {
$this->log("Empty ID.");
return [];
throw BadArgumentType::create(1, 'string');
}
if (!is_string($id)) {
$this->throwBadArgumentType(2, 'string');
throw BadArgumentType::create(2, 'string');
}
if (!$link || !is_string($link)) {
$this->throwBadArgumentType(3, 'string');
throw BadArgumentType::create(3, 'string');
}
if (!is_int($limit)) {
$this->throwBadArgumentType(4, 'string');
throw BadArgumentType::create(4, 'int');
}
if ($orderBy !== null && !is_string($orderBy)) {
throw BadArgumentType::create(5, 'string|null');
}
if ($order !== null && !is_string($order) && !is_bool($order)) {
throw BadArgumentType::create(6, 'string|bool|null');
}
$this->assertLinkRead($entityType, $link);
$entity = $entityManager->getEntityById($entityType, $id);
if (!$entity) {
$this->log("record\\findRelatedMany: Entity $entityType $id not found.", 'notice');
return [];
}
$metadata = $this->metadata;
if (!$orderBy) {
$orderBy = $metadata->get(['entityDefs', $entityType, 'collection', 'orderBy']);
if (is_null($order)) {
$order = $metadata->get(['entityDefs', $entityType, 'collection', 'order']) ?? 'asc';
}
} else {
$order = $order ?? 'asc';
}
if (!$entity instanceof CoreEntity) {
$this->throwError("Only core entities are supported.");
throw new Error("Non-core entity.");
}
$relationType = $entity->getRelationParam($link, 'type');
$relationType = $entity->getRelationType($link);
if (
in_array($relationType, [
@@ -148,42 +136,45 @@ class FindRelatedManyType extends BaseFunction implements
RelationType::BELONGS_TO_PARENT,
])
) {
$this->throwError("Not supported link type '$relationType'.");
throw new NotAllowedUsage("Not supported link type '$relationType'.");
}
$foreignEntityType = $entity->getRelationParam($link, RelationParam::ENTITY);
if (!$foreignEntityType) {
$this->throwError("Bad or not supported link '$link'.");
throw new NotAllowedUsage("Bad or not supported link '$link'.");
}
$foreignLink = $entity->getRelationParam($link, RelationParam::FOREIGN);
if (!$foreignLink) {
$this->throwError("Not supported link '$link'.");
throw new NotAllowedUsage("Not supported link '$link'.");
}
$builder = $this->injectableFactory->create(SelectBuilderFactory::class)
$builder = $this->selectBuilderFactory
->create()
->forUser($this->user)
->withPrimaryFilter(All::NAME)
->from($foreignEntityType);
$this->findQueryUtil->applyOrder($builder, $orderBy, $order, 6);
$whereClause = [];
if (count($args) <= 7) {
if (count($arguments) <= 7) {
$filter = null;
if (count($args) == 7) {
$filter = $args[6];
if (count($arguments) == 7) {
$filter = $arguments[6];
}
(new FindQueryUtil())->applyFilter($builder, $filter, 7);
$this->findQueryUtil->applyFilter($builder, $filter, 7);
} else {
$i = 6;
while ($i < count($args) - 1) {
$key = $args[$i];
$value = $args[$i + 1];
while ($i < count($arguments) - 1) {
$key = $arguments[$i];
$value = $arguments[$i + 1];
$this->findQueryUtil->assertWhereClauseKeyValid($entityType, $key);
$whereClause[] = [$key => $value];
@@ -194,7 +185,7 @@ class FindRelatedManyType extends BaseFunction implements
try {
$queryBuilder = $builder->buildQueryBuilder();
} catch (BadRequest|Forbidden $e) {
throw new Error($e->getMessage(), 0, $e);
throw new NotAllowedUsage($e->getMessage(), 0, $e);
}
if (!empty($whereClause)) {
@@ -218,10 +209,6 @@ class FindRelatedManyType extends BaseFunction implements
$queryBuilder->limit(0, $limit);
if ($orderBy) {
$queryBuilder->order($orderBy, $order);
}
$collection = $entityManager
->getRDBRepository($foreignEntityType)
->clone($queryBuilder->build())
@@ -239,8 +226,16 @@ class FindRelatedManyType extends BaseFunction implements
private function generateRandomString(): string
{
$generator = $this->injectableFactory->create(RandomStringGenerator::class);
return $this->randomStringGenerator->generate();
}
return $generator->generate();
/**
* @throws NotAllowedUsage
*/
private function assertLinkRead(string $entityType, string $link): void
{
if (!$this->systemRestriction->checkLinkRead($entityType, $link) ) {
throw new NotAllowedUsage("Cannot read restricted link $entityType.$link.");
}
}
}

View File

@@ -29,83 +29,92 @@
namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\Select\Primary\Filters\All;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\EntityManager;
use Espo\ORM\Name\Attribute;
use Espo\Core\Formula\ArgumentList;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Formula\Functions\RecordGroup\Util\FindQueryUtil;
use Espo\Core\Di;
use Espo\ORM\Query\Part\Order;
use Espo\ORM\Type\RelationType;
/**
* @noinspection PhpUnused
*/
class FindRelatedOneType extends BaseFunction implements
Di\EntityManagerAware,
Di\MetadataAware,
Di\InjectableFactoryAware,
Di\UserAware
class FindRelatedOneType implements Func
{
use Di\EntityManagerSetter;
use Di\MetadataSetter;
use Di\InjectableFactorySetter;
use Di\UserSetter;
public function __construct(
private EntityManager $entityManager,
private SelectBuilderFactory $selectBuilderFactory,
private FindQueryUtil $findQueryUtil,
private SystemRestriction $systemRestriction,
) {}
public function process(ArgumentList $args)
public function process(EvaluatedArgumentList $arguments): ?string
{
if (count($args) < 3) {
$this->throwTooFewArguments(3);
if (count($arguments) < 3) {
throw TooFewArguments::create(3);
}
$entityManager = $this->entityManager;
$entityType = $this->evaluate($args[0]);
$id = $this->evaluate($args[1]);
$link = $this->evaluate($args[2]);
$entityType = $arguments[0];
$id = $arguments[1];
$link = $arguments[2];
$orderBy = null;
$order = null;
if (count($args) > 3) {
$orderBy = $this->evaluate($args[3]);
if (count($arguments) > 3) {
$orderBy = $arguments[3];
}
if (count($args) > 4) {
$order = $this->evaluate($args[4]) ?? null;
if (count($arguments) > 4) {
$order = $arguments[4];
}
if (!$entityType) {
$this->throwBadArgumentType(1, 'string');
throw BadArgumentType::create(1, 'string');
}
if (!$id) {
return null;
if (!is_string($id)) {
throw BadArgumentType::create(2, 'string');
}
if (!$link) {
$this->throwBadArgumentType(3, 'string');
if (!is_string($link)) {
throw BadArgumentType::create(3, 'string');
}
if ($orderBy !== null && !is_string($orderBy)) {
throw BadArgumentType::create(4, 'string|null');
}
if ($order !== null && !is_string($order) && !is_bool($order)) {
throw BadArgumentType::create(5, 'string|bool|null');
}
$this->assertLinkRead($entityType, $link);
$entity = $entityManager->getEntityById($entityType, $id);
if (!$entity) {
return null;
}
$metadata = $this->metadata;
if (!$entity instanceof CoreEntity) {
$this->throwError("Only core entities are supported.");
throw new Error("Non-core entity.");
}
$relationType = $entity->getRelationParam($link, 'type');
$relationType = $entity->getRelationType($link);
if (
in_array($relationType, [
@@ -120,57 +129,46 @@ class FindRelatedOneType extends BaseFunction implements
->select([Attribute::ID])
->findOne();
if (!$relatedEntity) {
return null;
}
return $relatedEntity->getId();
}
if (!$orderBy) {
$orderBy = $metadata->get(['entityDefs', $entityType, 'collection', 'orderBy']);
if (is_null($order)) {
$order = $metadata->get(['entityDefs', $entityType, 'collection', 'order']) ?? 'ASC';
}
} else {
$order = $order ?? Order::ASC;
return $relatedEntity?->getId();
}
$foreignEntityType = $entity->getRelationParam($link, RelationParam::ENTITY);
if (!$foreignEntityType) {
$this->throwError("Bad or not supported link '$link'.");
throw new NotAllowedUsage("Bad or not supported link '$link'.");
}
$foreignLink = $entity->getRelationParam($link, RelationParam::FOREIGN);
if (!$foreignLink) {
$this->throwError("Not supported link '$link'.");
throw new NotAllowedUsage("Not supported link '$link'.");
}
$builder = $this->injectableFactory->create(SelectBuilderFactory::class)
$builder = $this->selectBuilderFactory
->create()
->forUser($this->user)
->withPrimaryFilter(All::NAME)
->from($foreignEntityType);
$this->findQueryUtil->applyOrder($builder, $orderBy, $order, 5);
$whereClause = [];
if (count($args) <= 6) {
if (count($arguments) <= 6) {
$filter = null;
if (count($args) === 6) {
$filter = $this->evaluate($args[5]);
if (count($arguments) === 6) {
$filter = $arguments[5];
}
(new FindQueryUtil())->applyFilter($builder, $filter, 6);
$this->findQueryUtil->applyFilter($builder, $filter, 6);
} else {
$i = 5;
while ($i < count($args) - 1) {
$key = $this->evaluate($args[$i]);
$value = $this->evaluate($args[$i + 1]);
while ($i < count($arguments) - 1) {
$key = $arguments[$i];
$value = $arguments[$i + 1];
$this->findQueryUtil->assertWhereClauseKeyValid($entityType, $key);
$whereClause[] = [$key => $value];
@@ -181,7 +179,7 @@ class FindRelatedOneType extends BaseFunction implements
try {
$queryBuilder = $builder->buildQueryBuilder();
} catch (BadRequest|Forbidden $e) {
throw new Error($e->getMessage(), 0, $e);
throw new NotAllowedUsage($e->getMessage(), 0, $e);
}
if (!empty($whereClause)) {
@@ -197,24 +195,26 @@ class FindRelatedOneType extends BaseFunction implements
$queryBuilder
->join($foreignLink)
->where([
$foreignLink . '.id' => $entity->getId(),
$foreignLink . '.' . Attribute::ID => $entity->getId(),
]);
}
if ($orderBy) {
$queryBuilder->order($orderBy, $order);
}
$relatedEntity = $entityManager
->getRDBRepository($foreignEntityType)
->clone($queryBuilder->build())
->select([Attribute::ID])
->findOne();
if ($relatedEntity) {
return $relatedEntity->getId();
}
return $relatedEntity?->getId();
}
return null;
/**
* @throws NotAllowedUsage
*/
private function assertLinkRead(string $entityType, string $link): void
{
if (!$this->systemRestriction->checkLinkRead($entityType, $link) ) {
throw new NotAllowedUsage("Cannot read restricted link $entityType.$link.");
}
}
}

View File

@@ -29,15 +29,23 @@
namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Formula\ArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Di;
use stdClass;
/**
* @noinspection PhpUnused
*/
class RelateType extends BaseFunction implements
Di\EntityManagerAware
Di\EntityManagerAware,
Di\InjectableFactoryAware
{
use Di\EntityManagerSetter;
use Di\InjectableFactorySetter;
public function process(ArgumentList $args)
{
@@ -52,21 +60,23 @@ class RelateType extends BaseFunction implements
$columnData = count($args) > 4 ? $this->evaluate($args[4]) : null;
if (!$entityType || !is_string($entityType)) {
$this->throwBadArgumentType(1, 'string');
throw BadArgumentType::create(1, 'string');
}
if (!$id) {
return null;
if (!is_string($id)) {
throw BadArgumentType::create(2, 'string');
}
if (!$link || !is_string($link)) {
$this->throwBadArgumentType(3, 'string');
throw BadArgumentType::create(3, 'string');
}
if ($columnData !== null && !$columnData instanceof stdClass) {
$this->throwBadArgumentType(4, 'object');
throw BadArgumentType::create(4, 'object');
}
$this->assertLinkWrite($entityType, $link);
if ($columnData instanceof stdClass) {
$columnData = get_object_vars($columnData);
}
@@ -105,4 +115,16 @@ class RelateType extends BaseFunction implements
return true;
}
/**
* @throws NotAllowedUsage
*/
private function assertLinkWrite(string $entityType, string $link): void
{
$restriction = $this->injectableFactory->create(SystemRestriction::class);
if (!$restriction->checkLinkWrite($entityType, $link) ) {
throw new NotAllowedUsage("Cannot write restricted link $entityType.$link.");
}
}
}

View File

@@ -29,17 +29,23 @@
namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Formula\{
Functions\BaseFunction,
ArgumentList,
};
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Formula\ArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Di;
/**
* @noinspection PhpUnused
*/
class RelationColumnType extends BaseFunction implements
Di\EntityManagerAware
Di\EntityManagerAware,
Di\InjectableFactoryAware
{
use Di\EntityManagerSetter;
use Di\InjectableFactorySetter;
public function process(ArgumentList $args)
{
@@ -55,26 +61,28 @@ class RelationColumnType extends BaseFunction implements
$foreignId = $args[3];
$column = $args[4];
if (!$entityType) {
$this->throwError("Empty entityType.");
if (!is_string($entityType)) {
throw BadArgumentType::create(1, 'string');
}
if (!$id) {
return null;
if (!is_string($id)) {
throw BadArgumentType::create(2, 'string');
}
if (!$link) {
$this->throwError("Empty link.");
if (!is_string($link)) {
throw BadArgumentType::create(3, 'string');
}
if (!$foreignId) {
return null;
if (!is_string($foreignId)) {
throw BadArgumentType::create(4, 'string');
}
if (!$column) {
$this->throwError("Empty column.");
if (!is_string($column)) {
throw BadArgumentType::create(5, 'string');
}
$this->assertLinkRead($entityType, $link);
$em = $this->entityManager;
if (!$em->hasRepository($entityType)) {
@@ -87,6 +95,19 @@ class RelationColumnType extends BaseFunction implements
return null;
}
return $em->getRelation($entity, $link)->getColumnById($foreignId, $column);
return $em->getRelation($entity, $link)
->getColumnById($foreignId, $column);
}
/**
* @throws NotAllowedUsage
*/
private function assertLinkRead(string $entityType, string $link): void
{
$restriction = $this->injectableFactory->create(SystemRestriction::class);
if (!$restriction->checkLinkRead($entityType, $link) ) {
throw new NotAllowedUsage("Cannot read restricted link $entityType.$link.");
}
}
}

View File

@@ -29,15 +29,20 @@
namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Formula\ArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Di;
class UnrelateType extends BaseFunction implements
Di\EntityManagerAware
Di\EntityManagerAware,
Di\InjectableFactoryAware
{
use Di\EntityManagerSetter;
use Di\InjectableFactorySetter;
public function process(ArgumentList $args)
{
@@ -50,22 +55,24 @@ class UnrelateType extends BaseFunction implements
$link = $this->evaluate($args[2]);
$foreignId = $this->evaluate($args[3]);
if (!$entityType) {
$this->throwError("Empty entityType.");
if (!is_string($entityType)) {
throw BadArgumentType::create(1, 'string');
}
if (!$id) {
return null;
if (!is_string($id)) {
throw BadArgumentType::create(2, 'string');
}
if (!$link) {
$this->throwError("Empty link.");
if (!is_string($link)) {
throw BadArgumentType::create(3, 'string');
}
if (!$foreignId) {
return null;
if (!is_string($foreignId)) {
throw BadArgumentType::create(4, 'string');
}
$this->assertLinkWrite($entityType, $link);
$em = $this->entityManager;
if (!$em->hasRepository($entityType)) {
@@ -84,4 +91,16 @@ class UnrelateType extends BaseFunction implements
return true;
}
/**
* @throws NotAllowedUsage
*/
private function assertLinkWrite(string $entityType, string $link): void
{
$restriction = $this->injectableFactory->create(SystemRestriction::class);
if (!$restriction->checkLinkWrite($entityType, $link) ) {
throw new NotAllowedUsage("Cannot write restricted link $entityType.$link.");
}
}
}

View File

@@ -29,17 +29,20 @@
namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Formula\{
Functions\BaseFunction,
ArgumentList,
};
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Formula\ArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Di;
class UpdateRelationColumnType extends BaseFunction implements
Di\EntityManagerAware
Di\EntityManagerAware,
Di\InjectableFactoryAware
{
use Di\EntityManagerSetter;
use Di\InjectableFactorySetter;
public function process(ArgumentList $args)
{
@@ -56,28 +59,30 @@ class UpdateRelationColumnType extends BaseFunction implements
$column = $args[4];
$value = $args[5];
if (!$entityType) {
$this->throwError("Empty entityType.");
if (!is_string($entityType)) {
throw BadArgumentType::create(1, 'string');
}
if (!$id) {
return null;
if (!is_string($id)) {
throw BadArgumentType::create(2, 'string');
}
if (!$link) {
$this->throwError("Empty link.");
if (!is_string($link)) {
throw BadArgumentType::create(3, 'string');
}
if (!$foreignId) {
return null;
}
if (!$column) {
$this->throwError("Empty column.");
if (!is_string($foreignId)) {
throw BadArgumentType::create(4, 'string');
}
if (!is_string($column)) {
$this->throwError("Column is not string.");
throw BadArgumentType::create(5, 'string');
}
$this->assertLinkWrite($entityType, $link);
if (!$column) {
$this->throwError("Empty column.");
}
$em = $this->entityManager;
@@ -98,4 +103,16 @@ class UpdateRelationColumnType extends BaseFunction implements
return true;
}
/**
* @throws NotAllowedUsage
*/
private function assertLinkWrite(string $entityType, string $link): void
{
$restriction = $this->injectableFactory->create(SystemRestriction::class);
if (!$restriction->checkLinkWrite($entityType, $link) ) {
throw new NotAllowedUsage("Cannot write restricted link $entityType.$link.");
}
}
}

View File

@@ -29,46 +29,52 @@
namespace Espo\Core\Formula\Functions\RecordGroup;
use Espo\Core\Formula\ArgumentList;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Di;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Espo\Core\Formula\Utils\EntityUtil;
use RuntimeException;
use Espo\ORM\EntityManager;
use stdClass;
/**
* @noinspection PhpUnused
*/
class UpdateType extends BaseFunction implements
Di\EntityManagerAware
class UpdateType implements Func
{
use Di\EntityManagerSetter;
public function __construct(
private EntityManager $entityManager,
private EntityUtil $entityUtil,
) {}
public function process(ArgumentList $args)
public function process(EvaluatedArgumentList $arguments): bool
{
if (count($args) < 2) {
$this->throwTooFewArguments(2);
if (count($arguments) < 2) {
throw TooFewArguments::create(2);
}
$args = $this->evaluate($args);
if (!is_array($args)) {
throw new RuntimeException();
}
$entityType = $args[0];
$id = $args[1];
$entityType = $arguments[0];
$id = $arguments[1];
if (!is_string($entityType)) {
$this->throwBadArgumentType(1, 'string');
throw BadArgumentType::create(1, 'string');
}
if (!is_string($id)) {
$this->throwBadArgumentType(2, 'string');
throw BadArgumentType::create(2, 'string');
}
$data = $this->getData($args, $entityType);
$data = $this->getData($arguments, $entityType);
$notAllowedAttributes = array_intersect(
array_keys($data),
$this->entityUtil->getWriteRestrictedAttributeList($entityType),
);
if ($notAllowedAttributes) {
throw new NotAllowedUsage("Cannot write $entityType.$notAllowedAttributes[0].");
}
$entity = $this->entityManager->getEntityById($entityType, $id);
@@ -76,9 +82,9 @@ class UpdateType extends BaseFunction implements
return false;
}
$entity->set($data);
$entity->setMultiple($data);
EntityUtil::checkUpdateAccess($entity);
$this->entityUtil->assertUpdateAccess($entity);
$this->entityManager->saveEntity($entity);
@@ -86,11 +92,10 @@ class UpdateType extends BaseFunction implements
}
/**
* @param array<int, mixed> $args
* @return array<string, mixed>
* @throws BadArgumentType
*/
private function getData(array $args, mixed $entityType): array
private function getData(EvaluatedArgumentList $args, mixed $entityType): array
{
if (count($args) >= 3 && $args[2] instanceof stdClass) {
return get_object_vars($args[2]);
@@ -104,7 +109,7 @@ class UpdateType extends BaseFunction implements
$attribute = $args[$i];
if (!is_string($entityType)) {
$this->throwBadArgumentType($i + 1, 'string');
throw BadArgumentType::create($i + 1, 'string');
}
/** @var string $attribute */

View File

@@ -29,17 +29,25 @@
namespace Espo\Core\Formula\Functions\RecordGroup\Util;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\BadArgumentValue;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Select\SearchParams;
use Espo\Core\Select\SelectBuilder;
use Espo\Core\Select\Where\Item;
use Espo\Core\Utils\Json;
use Espo\ORM\Query\Part\Order;
use Espo\ORM\QueryComposer\Util;
use InvalidArgumentException;
use stdClass;
class FindQueryUtil
{
public function __construct() {}
public function __construct(
private SystemRestriction $systemRestriction,
) {}
/**
* @throws Error
@@ -71,4 +79,68 @@ class FindQueryUtil
throw BadArgumentType::create($position, 'string|object');
}
}
/**
* @throws BadArgumentValue
*/
public function applyOrder(
SelectBuilder $builder,
?string $orderBy,
string|bool|null $order,
int $orderPosition,
): void {
if (is_bool($order)) {
$order = $order ? Order::DESC : Order::ASC;
}
if (is_string($order)) {
$order = strtoupper($order);
if ($order !== Order::ASC && $order !== Order::DESC) {
throw BadArgumentValue::create($orderPosition, "Order must be 'ASC'|'DESC'|bool|null.");
}
}
if ($orderBy) {
$builder->withSearchParams(
SearchParams::create()
->withOrderBy($orderBy)
->withOrder($order)
);
return;
}
if ($order !== null) {
$builder->withSearchParams(
SearchParams::create()
->withOrder($order)
);
}
$builder->withDefaultOrder();
}
/**
* @throws NotAllowedUsage
*/
public function assertWhereClauseKeyValid(string $entityType, string $key): void
{
try {
[$expression] = Util::splitWhereKeyThrowing($key);
} catch (InvalidArgumentException) {
throw new NotAllowedUsage("Not allowed where key expression '$key'");
}
if (Util::isComplexExpression($expression)) {
throw new NotAllowedUsage("Not allowed expression is where key '$key'");
}
$attribute = $expression;
if (!$this->systemRestriction->checkAttributeRead($entityType, $attribute)) {
throw new NotAllowedUsage("Cannot use restricted attribute in where key '$key'.");
}
}
}

View File

@@ -29,13 +29,33 @@
namespace Espo\Core\Formula\Functions;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Processor;
use Espo\Core\Formula\Utils\EntityUtil;
use Espo\ORM\Entity;
use Espo\ORM\Name\Attribute;
use stdClass;
class SetAttributeType extends Base
{
public function __construct(
private EntityUtil $entityUtil,
string $name,
Processor $processor,
?Entity $entity = null,
?stdClass $variables = null,
) {
parent::__construct(
name: $name,
processor: $processor,
entity: $entity,
variables: $variables,
);
}
/**
* @return mixed
* @throws Error
@@ -43,26 +63,32 @@ class SetAttributeType extends Base
public function process(stdClass $item)
{
if (count($item->value) < 2) {
throw new Error("SetAttribute: Too few arguments.");
throw TooFewArguments::create(2);
}
$name = $this->evaluate($item->value[0]);
$attribute = $this->evaluate($item->value[0]);
if (!is_string($name)) {
throw new Error("SetAttribute: First argument is not string.");
if (!is_string($attribute)) {
throw BadArgumentType::create(1, 'string');
}
if ($name === Attribute::ID) {
throw new Error("Formula set-attribute: Not allowed to set `id` attribute.");
if ($attribute === Attribute::ID) {
throw new NotAllowedUsage("Not allowed to set `id` attribute.");
}
$value = $this->evaluate($item->value[1]);
$entity = $this->getEntity();
$entity->set($name, $value);
$entityType = $entity->getEntityType();
EntityUtil::checkUpdateAccess($entity);
if (in_array($attribute, $this->entityUtil->getWriteRestrictedAttributeList($entityType))) {
throw new NotAllowedUsage("Cannot write $entityType.$attribute.");
}
$entity->set($attribute, $value);
$this->entityUtil->assertUpdateAccess($entity);
return $value;
}

View File

@@ -29,8 +29,9 @@
namespace Espo\Core\Formula\Utils;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Acl\Exceptions\Restricted;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Entities\User;
use Espo\ORM\Entity;
/**
@@ -39,46 +40,39 @@ use Espo\ORM\Entity;
*/
class EntityUtil
{
public function __construct(
private SystemRestriction $systemRestriction,
) {}
/**
* @throws NotAllowedUsage
*/
public static function checkUpdateAccess(Entity $entity): void
public function assertUpdateAccess(Entity $entity): void
{
if ($entity instanceof User) {
$restrictedTypeList = self::getUserRestrictedTypeList();
if (
$entity->isAttributeChanged(User::ATTR_TYPE) &&
(
in_array($entity->getFetched(User::ATTR_TYPE), $restrictedTypeList) ||
in_array($entity->getType(), $restrictedTypeList)
)
) {
throw new NotAllowedUsage("Cannot change user type.");
}
try {
$this->systemRestriction->assertUpdate($entity);
} catch (Restricted $e) {
throw new NotAllowedUsage($e->getMessage(), previous: $e);
}
}
/**
* @throws NotAllowedUsage
*/
public static function checkRemoveAccess(Entity $entity): void
public function assertRemoveAccess(Entity $entity): void
{
if ($entity instanceof User) {
if (in_array($entity->getType(), self::getUserRestrictedTypeList())) {
throw new NotAllowedUsage("Cannot remove the user.");
}
try {
$this->systemRestriction->assertRemoval($entity);
} catch (Restricted $e) {
throw new NotAllowedUsage($e->getMessage(), previous: $e);
}
}
/**
* @return string[]
*/
private static function getUserRestrictedTypeList(): array
public function getWriteRestrictedAttributeList(string $entityType): array
{
return [
User::TYPE_SUPER_ADMIN,
User::TYPE_SYSTEM,
];
return $this->systemRestriction->getWriteRestrictedAttributeList($entityType);
}
}

View File

@@ -0,0 +1,65 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 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\Core\Select\Helpers;
use Espo\ORM\BaseEntity;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Entity;
/**
* @internal
*/
class EntityHelper
{
public function __construct(
private Defs $defs,
) {}
/**
* @internal
*/
public function getRelationEntityType(Entity $entity, string $relation): ?string
{
if ($entity instanceof BaseEntity) {
return $entity->getRelationParam($relation, RelationParam::ENTITY);
}
$entityDefs = $this->defs->getEntity($entity->getEntityType());
if (!$entityDefs->hasRelation($relation)) {
return null;
}
return $entityDefs
->getRelation($relation)
->tryGetForeignEntityType();
}
}

View File

@@ -53,6 +53,7 @@ class Applier
private OrdererFactory $ordererFactory,
private AclManager $aclManager,
private User $user,
private Acl\SystemRestriction $systemRestriction,
) {}
/**
@@ -91,6 +92,13 @@ class Applier
) {
throw new Forbidden("Not access to order by field '$orderBy'.");
}
if (
!$params->applyPermissionCheck() &&
!$this->systemRestriction->checkFieldRead($this->entityType, $orderBy)
) {
throw new Forbidden("Cannot order by restricted field '$orderBy'.");
}
}
if ($orderBy === null) {

View File

@@ -32,14 +32,13 @@ namespace Espo\Core\Select\Where;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Acl;
use Espo\Core\Select\Helpers\EntityHelper;
use Espo\Core\Select\Where\Item\Type;
use Espo\Entities\Team;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\QueryComposer\Util;
use Espo\ORM\QueryComposer\Util as QueryUtil;
use Espo\ORM\EntityManager;
use Espo\ORM\Entity;
use Espo\ORM\BaseEntity;
/**
* Checks Where parameters. Throws an exception if anything not allowed is met.
@@ -93,6 +92,8 @@ class Checker
private string $entityType,
private EntityManager $entityManager,
private Acl $acl,
private Acl\SystemRestriction $systemRestriction,
private EntityHelper $entityHelper,
) {}
/**
@@ -137,9 +138,7 @@ class Checker
foreach ($argumentList as $argument) {
$this->checkAttributeExistence($argument, $type);
if ($checkWherePermission) {
$this->checkAttributePermission($argument, $type, $value);
}
$this->checkAttributePermission($argument, $type, $value, $checkWherePermission);
}
}
@@ -177,40 +176,37 @@ class Checker
* @throws Forbidden
* @throws BadRequest
*/
private function checkAttributePermission(string $attribute, string $type, mixed $value): void
private function checkAttributePermission(string $attribute, string $type, mixed $value, bool $aclCheck): void
{
$entityType = $this->entityType;
if (str_contains($attribute, '.')) {
[$link, $attribute] = explode('.', $attribute);
if (!$this->getSeed()->hasRelation($link)) {
// TODO allow alias
throw new Forbidden("Bad relation '$link' in where.");
}
$foreignEntityType = $this->getRelationEntityType($this->getSeed(), $link);
if (!$foreignEntityType) {
throw new Forbidden("Bad relation '$link' in where.");
}
if (
!$this->acl->checkScope($foreignEntityType) ||
in_array($link, $this->acl->getScopeForbiddenLinkList($entityType))
) {
throw new Forbidden("Forbidden relation '$link' in where.");
}
if (in_array($attribute, $this->acl->getScopeForbiddenAttributeList($foreignEntityType))) {
throw new Forbidden("Forbidden attribute '$link.$attribute' in where.");
}
$this->checkAttributePermissionWithLink(
entityType: $entityType,
aclCheck: $aclCheck,
link: $link,
attribute: $attribute,
);
return;
}
if (in_array($type, $this->linkTypeList)) {
$this->checkLink($type, $entityType, $attribute, $value);
$this->checkLink(
type: $type,
entityType: $entityType,
link: $attribute,
value: $value,
aclCheck: $aclCheck,
);
return;
}
if (!$aclCheck) {
$this->assertAttributeSystemRead($entityType, $attribute);
return;
}
@@ -220,6 +216,58 @@ class Checker
}
}
/**
* @throws Forbidden
* @throws BadRequest
*/
private function checkAttributePermissionWithLink(
string $entityType,
bool $aclCheck,
string $link,
string $attribute,
): void {
if (!$link) {
throw new BadRequest("Empty relation in path in where.");
}
if (!$attribute) {
throw new BadRequest("Empty attribute in path in where.");
}
if (!$this->getSeed()->hasRelation($link)) {
// TODO allow alias
throw new Forbidden("Bad relation '$link' in where.");
}
$foreignEntityType = $this->getRelationEntityType($this->getSeed(), $link);
if (!$foreignEntityType) {
throw new Forbidden("Bad relation '$link' in where.");
}
if (!$aclCheck) {
$this->assertLinkSystemRead($entityType, $link);
if (!$this->systemRestriction->checkAttributeRead($foreignEntityType, $attribute)) {
throw new Forbidden("System restricted attribute '$link.$attribute' in where.");
}
return;
}
if (
!$this->acl->checkScope($foreignEntityType) ||
in_array($link, $this->acl->getScopeForbiddenLinkList($entityType))
) {
throw new Forbidden("Forbidden relation '$link' in where.");
}
if (in_array($attribute, $this->acl->getScopeForbiddenAttributeList($foreignEntityType))) {
throw new Forbidden("Forbidden attribute '$link.$attribute' in where.");
}
}
private function getSeed(): Entity
{
$this->seed ??= $this->entityManager->getNewEntity($this->entityType);
@@ -227,29 +275,23 @@ class Checker
return $this->seed;
}
private function getRelationEntityType(Entity $entity, string $relation): mixed
private function getRelationEntityType(Entity $entity, string $relation): ?string
{
if ($entity instanceof BaseEntity) {
return $entity->getRelationParam($relation, RelationParam::ENTITY);
}
$entityDefs = $this->entityManager
->getDefs()
->getEntity($entity->getEntityType());
if (!$entityDefs->hasRelation($relation)) {
return null;
}
return $entityDefs->getRelation($relation)->getParam(RelationParam::ENTITY);
return $this->entityHelper->getRelationEntityType($entity, $relation);
}
/**
* @throws BadRequest
* @throws Forbidden
*/
private function checkLink(string $type, string $entityType, string $link, mixed $value): void
{
private function checkLink(
string $type,
string $entityType,
string $link,
mixed $value,
bool $aclCheck,
): void {
if (!$this->getSeed()->hasRelation($link)) {
throw new Forbidden("Bad relation '$link' in where.");
}
@@ -257,19 +299,24 @@ class Checker
$foreignEntityType = $this->getRelationEntityType($this->getSeed(), $link);
if (!$foreignEntityType) {
throw new Forbidden("Bad relation '$link' in where.");
throw new Forbidden("Bad relation '$link' in where, no foreign entity type.");
}
if ($type === self::TYPE_IS_USER_FROM_TEAMS) {
$foreignEntityType = Team::ENTITY_TYPE;
}
if (
in_array($link, $this->acl->getScopeForbiddenFieldList($entityType)) ||
!$this->acl->checkScope($foreignEntityType) ||
in_array($link, $this->acl->getScopeForbiddenLinkList($entityType))
) {
throw new Forbidden("Forbidden relation '$link' in where.");
if ($aclCheck) {
if (
in_array($link, $this->acl->getScopeForbiddenFieldList($entityType)) ||
!$this->acl->checkScope($foreignEntityType) ||
in_array($link, $this->acl->getScopeForbiddenLinkList($entityType))
) {
throw new Forbidden("Forbidden link '$link' in where.");
}
} else {
$this->assertLinkSystemRead($entityType, $link);
$this->assertFieldSystemRead($entityType, $link);
}
if (!in_array($type, $this->linkWithIdsTypeList)) {
@@ -299,9 +346,39 @@ class Checker
throw new Forbidden("Record '$foreignEntityType' `$id` not found.");
}
if (!$this->acl->checkEntityRead($entity)) {
if ($aclCheck && !$this->acl->checkEntityRead($entity)) {
throw new Forbidden("No access to '$foreignEntityType' `$id`.");
}
}
}
/**
* @throws Forbidden
*/
private function assertLinkSystemRead(string $entityType, string $link): void
{
if (!$this->systemRestriction->checkLinkRead($entityType, $link)) {
throw new Forbidden("System restricted link '$link' in where.");
}
}
/**
* @throws Forbidden
*/
private function assertFieldSystemRead(string $entityType, string $field): void
{
if (!$this->systemRestriction->checkFieldRead($entityType, $field)) {
throw new Forbidden("System restricted field '$field' in where.");
}
}
/**
* @throws Forbidden
*/
private function assertAttributeSystemRead(string $entityType, string $attribute): void
{
if (!$this->systemRestriction->checkAttributeRead($entityType, $attribute)) {
throw new Forbidden("System restricted attribute '$attribute' in where.");
}
}
}

View File

@@ -108,32 +108,6 @@ abstract class BaseQueryComposer implements QueryComposer
protected const EXISTS_OPERATOR = 'EXISTS';
/** @var string[] */
private array $comparisonOperators = [
'!=s',
'=s',
'!=',
'!*',
'*',
'>=',
'<=',
'>',
'<',
'=',
'>=any',
'<=any',
'>any',
'<any',
'!=any',
'=any',
'>=all',
'<=all',
'>all',
'<all',
'!=all',
'=all',
];
/** @var array<string, string> */
protected array $comparisonOperatorMap = [
'!=s' => 'NOT IN',
@@ -2522,23 +2496,11 @@ abstract class BaseQueryComposer implements QueryComposer
*/
private function splitWhereLeftItem(string $item): array
{
if (preg_match('/^[a-z0-9]+$/i', $item)) {
return [$item, '=', '='];
}
[$expression, $operator] = Util::splitWhereKey($item);
foreach ($this->comparisonOperators as $operator) {
$sqlOperator = $this->comparisonOperatorMap[$operator] ?? $operator;
$sqlOperator = $this->comparisonOperatorMap[$operator] ?? $operator;
if (!str_ends_with($item, $operator)) {
continue;
}
$expression = trim(substr($item, 0, -strlen($operator)));
return [$expression, $sqlOperator, $operator];
}
return [$item, '=', '='];
return [$expression, $sqlOperator, $operator];
}
/**

View File

@@ -29,8 +29,74 @@
namespace Espo\ORM\QueryComposer;
use InvalidArgumentException;
class Util
{
/** @var string[] */
private const array COMPARISON_OPERATORS = [
'!=s',
'=s',
'!=',
'!*',
'*',
'>=',
'<=',
'>',
'<',
'=',
'>=any',
'<=any',
'>any',
'<any',
'!=any',
'=any',
'>=all',
'<=all',
'>all',
'<all',
'!=all',
'=all',
];
/**
* @return array{0: string, 1: string}
* @internal
* @since 9.4.0
*/
public static function splitWhereKey(string $whereKey): array
{
try {
return self::splitWhereKeyThrowing($whereKey);
} catch (InvalidArgumentException) {}
return [$whereKey, '='];
}
/**
* @return array{0: string, 1: string}
* @internal
* @since 9.4.0
*/
public static function splitWhereKeyThrowing(string $whereKey): array
{
if (preg_match('/^[a-z0-9]+$/i', $whereKey)) {
return [$whereKey, '='];
}
foreach (self::COMPARISON_OPERATORS as $operator) {
if (!str_ends_with($whereKey, $operator)) {
continue;
}
$expression = trim(substr($whereKey, 0, -strlen($operator)));
return [$expression, $operator];
}
throw new InvalidArgumentException();
}
public static function isComplexExpression(string $string): bool
{
if (

View File

@@ -0,0 +1,3 @@
{
"systemWriteForbidden": true
}

View File

@@ -30,5 +30,6 @@
"modifiedAt": {
"readOnly": true
}
}
},
"systemWriteForbidden": true
}

View File

@@ -0,0 +1,3 @@
{
"systemWriteForbidden": true
}

View File

@@ -0,0 +1,3 @@
{
"systemWriteForbidden": true
}

View File

@@ -0,0 +1,3 @@
{
"systemWriteForbidden": true
}

View File

@@ -0,0 +1,7 @@
{
"fields": {
"auth2FATotpSecret": {
"forbidden": true
}
}
}

View File

@@ -62,6 +62,10 @@
}
}
}
},
"systemWriteForbidden": {
"type": "boolean",
"description": "Restricts record update and removal in formula and other potential configuration tools. As of v9.4."
}
}
}

View File

@@ -32,6 +32,7 @@ namespace tests\integration\Espo\Core\Formula;
use Espo\Core\Acl\Table;
use Espo\Core\Field\DateTimeOptional;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\NotAllowedUsage;
use Espo\Core\Formula\Exceptions\UnsafeFunction;
use Espo\Core\Formula\Manager;
use Espo\Entities\User;
@@ -44,7 +45,7 @@ use tests\integration\Core\BaseTestCase;
class FormulaTest extends BaseTestCase
{
public function testCountRelatedAndSumRelated()
public function testCountRelatedAndSumRelated(): void
{
$entityManager = $this->getContainer()->getByClass(EntityManager::class);
@@ -252,7 +253,7 @@ class FormulaTest extends BaseTestCase
$this->assertEquals(null, $result);
}
public function testFindMany(): void
public function testRecordFindMany(): void
{
$fm = $this->getContainer()->getByClass(Manager::class);
$em = $this->getContainer()->getByClass(EntityManager::class);
@@ -330,7 +331,7 @@ class FormulaTest extends BaseTestCase
'parentId' => $account->getId(),
]);
$c0 = $em->createEntity('Contact', [
$em->createEntity('Contact', [
'lastName' => '0',
]);
@@ -1195,4 +1196,370 @@ class FormulaTest extends BaseTestCase
$result = $fm->run($script, $account1);
$this->assertEquals(100.0, $result);
}
/**
* @noinspection PhpUnhandledExceptionInspection
*/
public function testRestrictedRead(): void
{
$em = $this->getEntityManager();
$fm = $this->getContainer()->getByClass(Manager::class);
$user = $em->createEntity(User::ENTITY_TYPE);
$userId = $user->getId();
$script = "record\\attribute('User', '$userId', 'password')";
$thrown = false;
try {
$fm->run($script);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
//
$script = "record\\attribute('User', '$userId', 'userDataId')";
$thrown = false;
try {
$fm->run($script);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
}
/**
* @noinspection PhpUnhandledExceptionInspection
*/
public function testRestrictedWrite(): void
{
$em = $this->getEntityManager();
$fm = $this->getContainer()->getByClass(Manager::class);
$user = $em->createEntity(User::ENTITY_TYPE);
$userId = $user->getId();
$script = "record\\update('User', '$userId', 'userDataId', '1')";
$thrown = false;
try {
$fm->run($script);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
}
/**
* @noinspection PhpUnhandledExceptionInspection
*/
public function testEntityFunctions(): void
{
$em = $this->getEntityManager();
$fm = $this->getContainer()->getByClass(Manager::class);
$account = $em->createEntity('Account');
/** @var Contact $contact */
$contact = $em->createEntity('Contact', ['accountId' => $account->getId()]);
$team = $em->createEntity('Team');
//
$script = "entity\\addLinkMultipleId('teams', '{$team->getId()}')";
$fm->run($script, $account);
$this->assertEquals([$team->getId()], $account->get('teamsIds'));
//
$script = "entity\\hasLinkMultipleId('teams', '{$team->getId()}')";
$result = $fm->run($script, $account);
$this->assertTrue($result);
//
$script = "entity\\removeLinkMultipleId('teams', '{$team->getId()}')";
$fm->run($script, $account);
$this->assertEquals([], $account->get('teamsIds'));
//
$script = "entity\\clearAttribute('teamsIds')";
$fm->run($script, $account);
$this->assertFalse($account->has('teamsIds'));
//
$script = "entity\\setLinkMultipleColumn('accounts', '{$account->getId()}', 'role', 'Tester')";
$fm->run($script, $contact);
$this->assertEquals('Tester', $contact->getLinkMultipleColumn('accounts', 'role', $account->getId()));
//
$em->saveEntity($contact);
$script = "entity\\getLinkColumn('accounts', '{$account->getId()}', 'role')";
$value = $fm->run($script, $contact);
$this->assertEquals('Tester', $value);
//
$script = "entity\\countRelated('accounts')";
$value = $fm->run($script, $contact);
$this->assertEquals(1, $value);
//
$script = "entity\\isRelated('accounts', '{$account->getId()}')";
$value = $fm->run($script, $contact);
$this->assertTrue($value);
}
/**
* @noinspection PhpUnhandledExceptionInspection
*/
public function testSystemRestriction(): void
{
$fm = $this->getContainer()->getByClass(Manager::class);
$user = $this->getContainer()->getByClass(User::class);
//
$script = "
record\\findOne('User', 'password', 'ASC');
";
$thrown = false;
try {
$fm->run($script);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
//
$script = "
record\\findOne('User', null, null, 'password', '1');
";
$thrown = false;
try {
$fm->run($script);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
//
$script = "
record\\findMany('User', 1, null, null, 'password', '1');
";
$thrown = false;
try {
$fm->run($script);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
//
$script = "
\$item = object\\create();
\$item['attribute'] = 'password';
\$item['type'] = 'equals';
\$item['value'] = '1';
record\\findOne('User', null, null, \$item);
";
$thrown = false;
try {
$fm->run($script);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
//
$script = "
record\\findRelatedOne('User', '{$user->getId()}', 'userData');
";
$thrown = false;
try {
$fm->run($script);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
//
$script = "
record\\findRelatedMany('User', '{$user->getId()}', 'userData', 1);
";
$thrown = false;
try {
$fm->run($script);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
//
$script = "
entity\\countRelated('userData');
";
$thrown = false;
try {
$fm->run($script, $user);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
//
$script = "
entity\\sumRelated('userData', 'id');
";
$thrown = false;
try {
$fm->run($script, $user);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
//
$script = "
\$a = password;
";
$thrown = false;
try {
$fm->run($script, $user);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
//
$script = "
authLogRecordId = '1';
";
$thrown = false;
try {
$fm->run($script, $user);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
//
$script = "
\$a = entity\\attribute('password');
";
$thrown = false;
try {
$fm->run($script, $user);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
//
$script = "
\$a = record\\attribute('User', '{$user->getId()}', 'password');
";
$thrown = false;
try {
$fm->run($script);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
//
$script = "
record\\fetch('User', '{$user->getId()}');
";
$data = $fm->run($script);
$this->assertFalse(property_exists($data, 'password'));
$this->assertTrue(property_exists($data, 'name'));
//
$script = "
record\\update('User', '{$user->getId()}', 'authLogRecordId' , '1');
";
$thrown = false;
try {
$fm->run($script, $user);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
//
$script = "
\$data = object\\create();
record\\create('Job', \$data);
";
$thrown = false;
try {
$fm->run($script, $user);
} catch (NotAllowedUsage) {
$thrown = true;
}
$this->assertTrue($thrown);
//
}
}

View File

@@ -29,12 +29,14 @@
namespace tests\unit\Espo\Core\Formula;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\FieldProcessing\SpecificFieldLoader;
use Espo\Core\Formula\Evaluator;
use Espo\Core\Formula\Exceptions\Error;
use Espo\Core\Formula\Exceptions\UndefinedKey;
use Espo\Core\Formula\Exceptions\UnsafeFunction;
use Espo\Core\Formula\Utils\EntityUtil;
use Espo\Core\InjectableFactory;
use Espo\Core\Formula\Exceptions\SyntaxError;
use Espo\Core\Utils\FieldUtil;
@@ -64,8 +66,17 @@ class EvaluatorTest extends TestCase
'fieldUtil' => $this->createMock(FieldUtil::class),
]);
$restriction = $this->createMock(SystemRestriction::class);
$restriction
->method('checkAttributeRead')
->willReturn(true);
$entityUtil = $this->createMock(EntityUtil::class);
$bindingContainer = BindingContainerBuilder::create()
->bindInstance(SpecificFieldLoader::class, $this->createMock(SpecificFieldLoader::class))
->bindInstance(SystemRestriction::class, $restriction)
->bindInstance(EntityUtil::class, $entityUtil)
->build();
$injectableFactory = new InjectableFactory($container, $bindingContainer);

View File

@@ -32,6 +32,7 @@
namespace tests\unit\Espo\Core\Formula;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\FieldProcessing\SpecificFieldLoader;
use Espo\Core\Formula\AttributeFetcher;
@@ -41,6 +42,7 @@ use Espo\Core\Formula\Parser\Ast\Value;
use Espo\Core\Formula\Parser\Ast\Variable;
use Espo\Core\Formula\Processor;
use Espo\Core\Formula\Argument;
use Espo\Core\Formula\Utils\EntityUtil;
use Espo\Core\Utils\DateTime;
use Espo\Core\Utils\FieldUtil;
use Espo\Core\Utils\NumberUtil;
@@ -48,11 +50,12 @@ use Espo\Core\Utils\Config;
use Espo\Core\Utils\Log;
use Espo\Core\ORM\EntityManager;
use Espo\Entities\User;
use Espo\Modules\Crm\Entities\Account;
use Espo\ORM\Entity as Entity;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\InjectableFactory;
use Espo\ORM\Repository\RDBRelation;
use Espo\ORM\Repository\RDBRepository;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use stdClass;
use tests\unit\ContainerMocker;
@@ -61,56 +64,18 @@ class FormulaTest extends TestCase
{
private $entity;
private $entityManager;
private $applicationConfig;
private $container;
private $injectableFactory;
private $restriction;
protected function setUp() : void
{
$this->entity = $this->getEntityMock();
$this->entityManager = $this->createMock(EntityManager::class);
date_default_timezone_set('UTC');
$dateTime = new DateTime();
$number = new NumberUtil();
$config = $this->createMock(Config::class);
$config
->expects($this->any())
->method('get')
->willReturnMap([
['timeZone', null, 'UTC']
]);
$this->applicationConfig = $this->createMock(Config\ApplicationConfig::class);
$this->applicationConfig
->expects($this->any())
->method('getTimeZone')
->willReturn('UTC');
$user = $this->createMock(User::class);
$log = $this->createMock(Log::class);
$user->set('id', '1');
$user
->expects($this->any())
->method('get')
->willReturnMap([
['id', '1']
]);
$containerMocker = new ContainerMocker($this);
$this->container = $containerMocker->create([
'entityManager' => $this->entityManager,
'dateTime' => $dateTime,
'number' => $number,
'config' => $config,
'user' => $user,
'log' => $log,
]);
$this->ininContainer();
}
private static function stringToNode(string $string): mixed
@@ -141,30 +106,28 @@ class FormulaTest extends TestCase
protected function createProcessor($variables = null, ?Entity $entity = null)
{
$injectableFactory = new InjectableFactory(
$this->container,
BindingContainerBuilder::create()
->bindInstance(Config\ApplicationConfig::class, $this->applicationConfig)
->build()
);
$fieldUtil = $this->createMock(FieldUtil::class);
$loader = $this->createMock(SpecificFieldLoader::class);
$attributeFetcher = new AttributeFetcher($this->entityManager, $fieldUtil, $loader);
$attributeFetcher = new AttributeFetcher(
entityManager: $this->entityManager,
fieldUtil: $fieldUtil,
specificFieldLoader: $loader,
systemRestriction: $this->restriction,
);
return new Processor(
$injectableFactory,
$attributeFetcher,
null,
$entity ?? $this->entity,
$variables
injectableFactory: $this->injectableFactory,
attributeFetcher: $attributeFetcher,
functionClassNameMap: null,
entity: $entity ?? $this->entity,
variables: $variables,
);
}
protected function getEntityMock()
protected function getEntityMock(): Entity & MockObject
{
return $this->getMockBuilder(Entity::class)->disableOriginalConstructor()->getMock();
return $this->createMock(Entity::class);
}
protected function setEntityAttributes($entity, $attributes)
@@ -313,74 +276,6 @@ class FormulaTest extends TestCase
$this->assertTrue($result);
}
function testAddLinkMultipleId()
{
$item = new Argument(self::stringToNode('
{
"type": "entity\\\\addLinkMultipleId",
"value": [
{
"type": "value",
"value": "teams"
},
{
"type": "value",
"value": "1"
}
]
}
'));
$entity = $this->createMock(CoreEntity::class);
$this->setEntityAttributes($entity, [
'teamsIds' => ['2']
]);
$entity
->expects($this->any())
->method('addLinkMultipleId')
->with('teams', '1');
$this->createProcessor(null, $entity)->process($item);
$this->assertTrue(true);
}
function testRemoveLinkMultipleId()
{
$item = new Argument(self::stringToNode('
{
"type": "entity\\\\removeLinkMultipleId",
"value": [
{
"type": "value",
"value": "teams"
},
{
"type": "value",
"value": "1"
}
]
}
'));
$entity = $this->createMock(CoreEntity::class);
$this->setEntityAttributes($entity, [
'teamsIds' => ['1', '2']
]);
$entity
->expects($this->any())
->method('removeLinkMultipleId')
->with('teams', '1');
$this->createProcessor(null, $entity)->process($item);
$this->assertTrue(true);
}
function testAnd()
{
$item = new Argument(self::stringToNode('
@@ -1004,16 +899,9 @@ class FormulaTest extends TestCase
'amount' => 3
]);
$repository = $this->createMock(RDBRepository::class);
$relation = $this->createMock(RDBRelation::class);
$this->entityManager
->expects($this->once())
->method('getRDBRepository')
->with($this->entity->getEntityType())
->willReturn($repository);
$repository
->expects($this->once())
->method('getRelation')
->with($this->entity, 'parent')
@@ -1363,29 +1251,6 @@ class FormulaTest extends TestCase
$this->createProcessor($variables)->process($item);
}
function testClearAttribute(): void
{
$item = new Argument(self::stringToNode('
{
"type": "entity\\\\clearAttribute",
"value": [
{
"type": "value",
"value": "amount"
}
]
}
'));
$this->entity
->expects($this->once())
->method('clear')
->with('amount');
/** @noinspection PhpUnhandledExceptionInspection */
$this->createProcessor((object) [])->process($item);
}
function testCompareDates()
{
$item = new Argument(self::stringToNode('
@@ -3173,14 +3038,94 @@ class FormulaTest extends TestCase
}
'));
$variables = (object)[];
$this->setEntityAttributes($this->entity, array(
$variables = (object) [];
$this->setEntityAttributes($this->entity, [
'test' => 'hello'
));
]);
$this->createProcessor($variables)->process($item);
$this->assertEquals(5, $variables->counter);
$this->assertEquals('hello', $variables->test);
}
/**
* @return void
* @throws \PHPUnit\Framework\MockObject\Exception
*/
private function ininContainer(): void
{
$dateTime = new DateTime();
$number = new NumberUtil();
$config = $this->createMock(Config::class);
$config
->expects($this->any())
->method('get')
->willReturnMap([
['timeZone', null, 'UTC']
]);
$restriction = $this->createMock(SystemRestriction::class);
$restriction
->method('checkAttributeRead')
->willReturn(true);
$restriction
->method('checkLinkRead')
->willReturn(true);
$restriction
->method('checkLinkWrite')
->willReturn(true);
$restriction
->method('checkFieldWrite')
->willReturn(true);
$this->restriction = $restriction;
$applicationConfig = $this->createMock(Config\ApplicationConfig::class);
$applicationConfig
->expects($this->any())
->method('getTimeZone')
->willReturn('UTC');
$user = $this->createMock(User::class);
$log = $this->createMock(Log::class);
$user->set('id', '1');
$user
->expects($this->any())
->method('get')
->willReturnMap([
['id', '1']
]);
$containerMocker = new ContainerMocker($this);
$container = $containerMocker->create([
'entityManager' => $this->entityManager,
'dateTime' => $dateTime,
'number' => $number,
'config' => $config,
'user' => $user,
'log' => $log,
]);
$entityUtil = $this->createMock(EntityUtil::class);
$this->injectableFactory = new InjectableFactory(
$container,
BindingContainerBuilder::create()
->bindInstance(Config\ApplicationConfig::class, $applicationConfig)
->bindInstance(SystemRestriction::class, $this->restriction)
->bindInstance(EntityUtil::class, $entityUtil)
->bindInstance(Entity::class, $this->entity)
->build()
);
}
}

View File

@@ -29,6 +29,7 @@
namespace tests\unit\Espo\Core\Select\Applier\Appliers;
use Espo\Core\Acl\SystemRestriction;
use Espo\Core\AclManager;
use Espo\Core\Exceptions\BadRequest;
use Espo\Entities\User;
@@ -68,15 +69,21 @@ class OrderApplierTest extends TestCase
$aclManager = $this->createMock(AclManager::class);
$user = $this->createMock(User::class);
$restriction = $this->createMock(SystemRestriction::class);
$restriction
->method('checkFieldRead')
->willReturn(true);
$this->entityType = 'Test';
$this->applier = new OrderApplier(
$this->entityType,
$this->metadataProvider,
$this->itemConverterFactory,
$this->ordererFactory,
$aclManager,
$user,
entityType: $this->entityType,
metadataProvider: $this->metadataProvider,
itemConverterFactory: $this->itemConverterFactory,
ordererFactory: $this->ordererFactory,
aclManager: $aclManager,
user: $user,
systemRestriction: $restriction,
);
}

View File

@@ -33,6 +33,7 @@ use Espo\Core\Acl;
use Espo\Core\Acl\Table;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Select\Helpers\EntityHelper;
use Espo\Core\Select\Where\Checker;
use Espo\Core\Select\Where\Item;
use Espo\Core\Select\Where\Params;
@@ -44,11 +45,12 @@ class CheckerTest extends TestCase
{
/** @var Checker|null */
protected $checker = null;
/** @var EntityManager|null */
protected $entityManager = null;
protected ?EntityManager $entityManager = null;
/** @var Acl|null */
protected $acl = null;
protected ?EntityHelper $entityHelper = null;
protected ?string $entityType = null;
protected ?string $foreignEntityType = null;
@@ -59,14 +61,30 @@ class CheckerTest extends TestCase
{
$this->entityManager = $this->createMock(EntityManager::class);
$this->acl = $this->createMock(Acl::class);
$systemRestriction = $this->createMock(Acl\SystemRestriction::class);
$this->entityHelper = $this->createMock(EntityHelper::class);
$systemRestriction
->method('checkAttributeRead')
->willReturn(true);
$systemRestriction
->method('checkLinkRead')
->willReturn(true);
$systemRestriction
->method('checkFieldRead')
->willReturn(true);
$this->entityType = 'Test';
$this->foreignEntityType = 'TestForeign';
$this->checker = new Checker(
$this->entityType,
$this->entityManager,
$this->acl,
entityType: $this->entityType,
entityManager: $this->entityManager,
acl: $this->acl,
systemRestriction: $systemRestriction,
entityHelper: $this->entityHelper,
);
$this->params = $this->createMock(Params::class);
@@ -124,8 +142,21 @@ class CheckerTest extends TestCase
['test2', true],
]);
$this->entityHelper
->method('getRelationEntityType')
->willReturnMap([
[$this->entity, 'test3', 'AnotherEntity'],
]);
$another = $this->createMock(Entity::class);
$this->entityManager
->method('getEntityById')
->willReturnMap([
['AnotherEntity', 'value3', $another]
]);
$this->entity
->expects($this->once())
->method('hasRelation')
->with('test3')
->willReturn(true);
@@ -297,6 +328,17 @@ class CheckerTest extends TestCase
[$foreign, true]
]);
$this->entity
->method('hasRelation')
->with('test3')
->willReturn(true);
$this->entityHelper
->method('getRelationEntityType')
->willReturnMap([
[$this->entity, 'test3', $this->foreignEntityType],
]);
$this->checker->check($item, $this->params);
$this->assertTrue(true);