diff --git a/application/Espo/Core/Acl/Exceptions/Restricted.php b/application/Espo/Core/Acl/Exceptions/Restricted.php new file mode 100644 index 0000000000..0976a7a8b8 --- /dev/null +++ b/application/Espo/Core/Acl/Exceptions/Restricted.php @@ -0,0 +1,38 @@ +. + * + * 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 +{} diff --git a/application/Espo/Core/Acl/SystemRestriction.php b/application/Espo/Core/Acl/SystemRestriction.php new file mode 100644 index 0000000000..f4fd349670 --- /dev/null +++ b/application/Espo/Core/Acl/SystemRestriction.php @@ -0,0 +1,231 @@ +. + * + * 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); + } +} diff --git a/application/Espo/Core/Formula/AttributeFetcher.php b/application/Espo/Core/Formula/AttributeFetcher.php index 4bd726c53f..67bd52f72c 100644 --- a/application/Espo/Core/Formula/AttributeFetcher.php +++ b/application/Espo/Core/Formula/AttributeFetcher.php @@ -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); + } } diff --git a/application/Espo/Core/Formula/Func.php b/application/Espo/Core/Formula/Func.php index fdcbb15dc7..6fa2b4466f 100644 --- a/application/Espo/Core/Formula/Func.php +++ b/application/Espo/Core/Formula/Func.php @@ -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 { diff --git a/application/Espo/Core/Formula/FunctionFactory.php b/application/Espo/Core/Formula/FunctionFactory.php index 4f05c90db4..8f83fea343 100644 --- a/application/Espo/Core/Formula/FunctionFactory.php +++ b/application/Espo/Core/Formula/FunctionFactory.php @@ -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, [ diff --git a/application/Espo/Core/Formula/Functions/AttributeType.php b/application/Espo/Core/Formula/Functions/AttributeType.php index d7cd42144d..755e370362 100644 --- a/application/Espo/Core/Formula/Functions/AttributeType.php +++ b/application/Espo/Core/Formula/Functions/AttributeType.php @@ -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) diff --git a/application/Espo/Core/Formula/Functions/Base.php b/application/Espo/Core/Formula/Functions/Base.php index a48f428859..154d60d2ba 100644 --- a/application/Espo/Core/Formula/Functions/Base.php +++ b/application/Espo/Core/Formula/Functions/Base.php @@ -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; diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/AddLinkMultipleIdType.php b/application/Espo/Core/Formula/Functions/EntityGroup/AddLinkMultipleIdType.php index 8a749ace66..39511bbd9d 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/AddLinkMultipleIdType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/AddLinkMultipleIdType.php @@ -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; } } diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/AttributeFetchedType.php b/application/Espo/Core/Formula/Functions/EntityGroup/AttributeFetchedType.php index 90324e936d..5de0520d00 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/AttributeFetchedType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/AttributeFetchedType.php @@ -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); diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/AttributeType.php b/application/Espo/Core/Formula/Functions/EntityGroup/AttributeType.php index 3533751a1e..360fd1729b 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/AttributeType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/AttributeType.php @@ -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); } } diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/ClearAttributeType.php b/application/Espo/Core/Formula/Functions/EntityGroup/ClearAttributeType.php index dfea8ee69b..188b298805 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/ClearAttributeType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/ClearAttributeType.php @@ -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; } diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/CountRelatedType.php b/application/Espo/Core/Formula/Functions/EntityGroup/CountRelatedType.php index 7a64f92f3d..b95b47eb50 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/CountRelatedType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/CountRelatedType.php @@ -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(); diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/GetLinkColumnType.php b/application/Espo/Core/Formula/Functions/EntityGroup/GetLinkColumnType.php index a2c9ad57ea..63a9874f75 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/GetLinkColumnType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/GetLinkColumnType.php @@ -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); } } diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/HasLinkMultipleIdType.php b/application/Espo/Core/Formula/Functions/EntityGroup/HasLinkMultipleIdType.php index f31f8c835f..4c7d888780 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/HasLinkMultipleIdType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/HasLinkMultipleIdType.php @@ -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); } } diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/IsAttributeChangedType.php b/application/Espo/Core/Formula/Functions/EntityGroup/IsAttributeChangedType.php index 46eec923ed..c1b087fe8a 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/IsAttributeChangedType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/IsAttributeChangedType.php @@ -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); } diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/IsAttributeNotChangedType.php b/application/Espo/Core/Formula/Functions/EntityGroup/IsAttributeNotChangedType.php index 5e4fc6d356..44eb699e0b 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/IsAttributeNotChangedType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/IsAttributeNotChangedType.php @@ -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) { diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/IsNewType.php b/application/Espo/Core/Formula/Functions/EntityGroup/IsNewType.php index dc0b21864d..5b822371fc 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/IsNewType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/IsNewType.php @@ -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) { diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/IsRelatedType.php b/application/Espo/Core/Formula/Functions/EntityGroup/IsRelatedType.php index 6f8d5dd876..f54a65de35 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/IsRelatedType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/IsRelatedType.php @@ -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); } } diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/RemoveLinkMultipleIdType.php b/application/Espo/Core/Formula/Functions/EntityGroup/RemoveLinkMultipleIdType.php index 77839175d6..679975d5a0 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/RemoveLinkMultipleIdType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/RemoveLinkMultipleIdType.php @@ -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; } } diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/SetLinkMultipleColumnType.php b/application/Espo/Core/Formula/Functions/EntityGroup/SetLinkMultipleColumnType.php index 2f1b6f6c20..6542742113 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/SetLinkMultipleColumnType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/SetLinkMultipleColumnType.php @@ -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; } } diff --git a/application/Espo/Core/Formula/Functions/EntityGroup/SumRelatedType.php b/application/Espo/Core/Formula/Functions/EntityGroup/SumRelatedType.php index f205d3aced..47f1959965 100644 --- a/application/Espo/Core/Formula/Functions/EntityGroup/SumRelatedType.php +++ b/application/Espo/Core/Formula/Functions/EntityGroup/SumRelatedType.php @@ -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); diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/AttributeType.php b/application/Espo/Core/Formula/Functions/RecordGroup/AttributeType.php index b5a2205252..991c0d04c8 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/AttributeType.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/AttributeType.php @@ -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); diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/CountType.php b/application/Espo/Core/Formula/Functions/RecordGroup/CountType.php index f4dfb4dae2..2c14fd41a4 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/CountType.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/CountType.php @@ -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]; diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/CreateType.php b/application/Espo/Core/Formula/Functions/RecordGroup/CreateType.php index 4dd55bfda6..5c900c4d18 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/CreateType.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/CreateType.php @@ -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 $args * @return array * @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 */ diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/DeleteType.php b/application/Espo/Core/Formula/Functions/RecordGroup/DeleteType.php index 8a4c76ca54..b31ef77bae 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/DeleteType.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/DeleteType.php @@ -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); diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/ExistsType.php b/application/Espo/Core/Formula/Functions/RecordGroup/ExistsType.php index 6dc9283f7f..44263f02a7 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/ExistsType.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/ExistsType.php @@ -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(); } diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/FetchType.php b/application/Espo/Core/Formula/Functions/RecordGroup/FetchType.php index 065794281d..89db695b99 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/FetchType.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/FetchType.php @@ -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(); } diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/FindManyType.php b/application/Espo/Core/Formula/Functions/RecordGroup/FindManyType.php index b3cb970c1e..60984cced9 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/FindManyType.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/FindManyType.php @@ -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); diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/FindOneType.php b/application/Espo/Core/Formula/Functions/RecordGroup/FindOneType.php index f167e19093..46c6e5a110 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/FindOneType.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/FindOneType.php @@ -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(); } } diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/FindRelatedManyType.php b/application/Espo/Core/Formula/Functions/RecordGroup/FindRelatedManyType.php index 0046a9711f..2c6279d4b9 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/FindRelatedManyType.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/FindRelatedManyType.php @@ -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."); + } } } diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/FindRelatedOneType.php b/application/Espo/Core/Formula/Functions/RecordGroup/FindRelatedOneType.php index 6fbb3d1335..1b480bc4eb 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/FindRelatedOneType.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/FindRelatedOneType.php @@ -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."); + } } } diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/RelateType.php b/application/Espo/Core/Formula/Functions/RecordGroup/RelateType.php index af074f0c1f..a870828888 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/RelateType.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/RelateType.php @@ -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."); + } + } } diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/RelationColumnType.php b/application/Espo/Core/Formula/Functions/RecordGroup/RelationColumnType.php index 7283500c49..84aa6d157b 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/RelationColumnType.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/RelationColumnType.php @@ -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."); + } } } diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/UnrelateType.php b/application/Espo/Core/Formula/Functions/RecordGroup/UnrelateType.php index e4e66e7ef4..b6d1c5a797 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/UnrelateType.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/UnrelateType.php @@ -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."); + } + } } diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/UpdateRelationColumnType.php b/application/Espo/Core/Formula/Functions/RecordGroup/UpdateRelationColumnType.php index 3bbef1f0a0..f18e3b574b 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/UpdateRelationColumnType.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/UpdateRelationColumnType.php @@ -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."); + } + } } diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/UpdateType.php b/application/Espo/Core/Formula/Functions/RecordGroup/UpdateType.php index 5c4a8ad620..29ff172257 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/UpdateType.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/UpdateType.php @@ -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 $args * @return array * @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 */ diff --git a/application/Espo/Core/Formula/Functions/RecordGroup/Util/FindQueryUtil.php b/application/Espo/Core/Formula/Functions/RecordGroup/Util/FindQueryUtil.php index d6d6a05ded..0913c3defd 100644 --- a/application/Espo/Core/Formula/Functions/RecordGroup/Util/FindQueryUtil.php +++ b/application/Espo/Core/Formula/Functions/RecordGroup/Util/FindQueryUtil.php @@ -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'."); + } + } } diff --git a/application/Espo/Core/Formula/Functions/SetAttributeType.php b/application/Espo/Core/Formula/Functions/SetAttributeType.php index 795e140e84..6d1ad88e38 100644 --- a/application/Espo/Core/Formula/Functions/SetAttributeType.php +++ b/application/Espo/Core/Formula/Functions/SetAttributeType.php @@ -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; } diff --git a/application/Espo/Core/Formula/Utils/EntityUtil.php b/application/Espo/Core/Formula/Utils/EntityUtil.php index 5c3f523997..db07dd0c5c 100644 --- a/application/Espo/Core/Formula/Utils/EntityUtil.php +++ b/application/Espo/Core/Formula/Utils/EntityUtil.php @@ -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); } } diff --git a/application/Espo/Core/Select/Helpers/EntityHelper.php b/application/Espo/Core/Select/Helpers/EntityHelper.php new file mode 100644 index 0000000000..8408863b95 --- /dev/null +++ b/application/Espo/Core/Select/Helpers/EntityHelper.php @@ -0,0 +1,65 @@ +. + * + * 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(); + } +} diff --git a/application/Espo/Core/Select/Order/Applier.php b/application/Espo/Core/Select/Order/Applier.php index 976d7f33ba..563756dc24 100644 --- a/application/Espo/Core/Select/Order/Applier.php +++ b/application/Espo/Core/Select/Order/Applier.php @@ -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) { diff --git a/application/Espo/Core/Select/Where/Checker.php b/application/Espo/Core/Select/Where/Checker.php index 99fe1a6092..5d1e0ac769 100644 --- a/application/Espo/Core/Select/Where/Checker.php +++ b/application/Espo/Core/Select/Where/Checker.php @@ -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."); + } + } } diff --git a/application/Espo/ORM/QueryComposer/BaseQueryComposer.php b/application/Espo/ORM/QueryComposer/BaseQueryComposer.php index 2baf9a1980..13eacf02c8 100644 --- a/application/Espo/ORM/QueryComposer/BaseQueryComposer.php +++ b/application/Espo/ORM/QueryComposer/BaseQueryComposer.php @@ -108,32 +108,6 @@ abstract class BaseQueryComposer implements QueryComposer protected const EXISTS_OPERATOR = 'EXISTS'; - /** @var string[] */ - private array $comparisonOperators = [ - '!=s', - '=s', - '!=', - '!*', - '*', - '>=', - '<=', - '>', - '<', - '=', - '>=any', - '<=any', - '>any', - '=all', - '<=all', - '>all', - ' */ 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]; } /** diff --git a/application/Espo/ORM/QueryComposer/Util.php b/application/Espo/ORM/QueryComposer/Util.php index e8a860b066..49948bb4fc 100644 --- a/application/Espo/ORM/QueryComposer/Util.php +++ b/application/Espo/ORM/QueryComposer/Util.php @@ -29,8 +29,74 @@ namespace Espo\ORM\QueryComposer; +use InvalidArgumentException; + class Util { + /** @var string[] */ + private const array COMPARISON_OPERATORS = [ + '!=s', + '=s', + '!=', + '!*', + '*', + '>=', + '<=', + '>', + '<', + '=', + '>=any', + '<=any', + '>any', + '=all', + '<=all', + '>all', + ' $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); + + // + } } diff --git a/tests/unit/Espo/Core/Formula/EvaluatorTest.php b/tests/unit/Espo/Core/Formula/EvaluatorTest.php index e6419f851f..360db3efff 100644 --- a/tests/unit/Espo/Core/Formula/EvaluatorTest.php +++ b/tests/unit/Espo/Core/Formula/EvaluatorTest.php @@ -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); diff --git a/tests/unit/Espo/Core/Formula/FormulaTest.php b/tests/unit/Espo/Core/Formula/FormulaTest.php index a835b98250..8c34407358 100644 --- a/tests/unit/Espo/Core/Formula/FormulaTest.php +++ b/tests/unit/Espo/Core/Formula/FormulaTest.php @@ -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() + ); + } } diff --git a/tests/unit/Espo/Core/Select/Applier/Appliers/OrderApplierTest.php b/tests/unit/Espo/Core/Select/Applier/Appliers/OrderApplierTest.php index 5e58ff2bc4..e4a85c0984 100644 --- a/tests/unit/Espo/Core/Select/Applier/Appliers/OrderApplierTest.php +++ b/tests/unit/Espo/Core/Select/Applier/Appliers/OrderApplierTest.php @@ -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, ); } diff --git a/tests/unit/Espo/Core/Select/Where/CheckerTest.php b/tests/unit/Espo/Core/Select/Where/CheckerTest.php index c3f47e8d08..3d87d0daf2 100644 --- a/tests/unit/Espo/Core/Select/Where/CheckerTest.php +++ b/tests/unit/Espo/Core/Select/Where/CheckerTest.php @@ -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);