Initial commit
This commit is contained in:
74
application/Espo/Core/Select/Where/Applier.php
Normal file
74
application/Espo/Core/Select/Where/Applier.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Select\Where\Item as WhereItem;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
use Espo\Entities\User;
|
||||
|
||||
class Applier
|
||||
{
|
||||
public function __construct(
|
||||
private string $entityType,
|
||||
private User $user,
|
||||
private ConverterFactory $converterFactory,
|
||||
private CheckerFactory $checkerFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function apply(QueryBuilder $queryBuilder, WhereItem $whereItem, Params $params): void
|
||||
{
|
||||
$this->check($whereItem, $params);
|
||||
|
||||
$converter = $this->converterFactory->create($this->entityType, $this->user);
|
||||
|
||||
$convertedParams = new Converter\Params(useSubQueryIfMany: true);
|
||||
|
||||
$queryBuilder->where(
|
||||
$converter->convert($queryBuilder, $whereItem, $convertedParams)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function check(Item $whereItem, Params $params): void
|
||||
{
|
||||
$checker = $this->checkerFactory->create($this->entityType, $this->user);
|
||||
|
||||
$checker->check($whereItem, $params);
|
||||
}
|
||||
}
|
||||
307
application/Espo/Core/Select/Where/Checker.php
Normal file
307
application/Espo/Core/Select/Where/Checker.php
Normal file
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Acl;
|
||||
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.
|
||||
*
|
||||
* @todo Check read access to foreign entity for belongs-to, belongs-to-parent, has-one.
|
||||
*/
|
||||
class Checker
|
||||
{
|
||||
private ?Entity $seed = null;
|
||||
|
||||
private const TYPE_IN_CATEGORY = 'inCategory';
|
||||
private const TYPE_IS_USER_FROM_TEAMS = 'isUserFromTeams';
|
||||
|
||||
/** @var string[] */
|
||||
private $nestingTypeList = [
|
||||
Type::OR,
|
||||
Type::AND,
|
||||
Type::NOT,
|
||||
Type::SUBQUERY_IN,
|
||||
Type::SUBQUERY_NOT_IN,
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private $subQueryTypeList = [
|
||||
Type::SUBQUERY_IN,
|
||||
Type::SUBQUERY_NOT_IN,
|
||||
Type::NOT,
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private $linkTypeList = [
|
||||
self::TYPE_IN_CATEGORY,
|
||||
self::TYPE_IS_USER_FROM_TEAMS,
|
||||
Type::IS_LINKED_WITH,
|
||||
Type::IS_NOT_LINKED_WITH,
|
||||
Type::IS_LINKED_WITH_ALL,
|
||||
Type::IS_LINKED_WITH_ANY,
|
||||
Type::IS_LINKED_WITH_NONE,
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private $linkWithIdsTypeList = [
|
||||
self::TYPE_IN_CATEGORY,
|
||||
self::TYPE_IS_USER_FROM_TEAMS,
|
||||
Type::IS_LINKED_WITH,
|
||||
Type::IS_NOT_LINKED_WITH,
|
||||
Type::IS_LINKED_WITH_ALL,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private string $entityType,
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Check.
|
||||
*
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function check(Item $item, Params $params): void
|
||||
{
|
||||
$this->checkItem($item, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function checkItem(Item $item, Params $params): void
|
||||
{
|
||||
$type = $item->getType();
|
||||
$attribute = $item->getAttribute();
|
||||
$value = $item->getValue();
|
||||
|
||||
$forbidComplexExpressions = $params->forbidComplexExpressions();
|
||||
$checkWherePermission = $params->applyPermissionCheck();
|
||||
|
||||
if ($forbidComplexExpressions) {
|
||||
if (in_array($type, $this->subQueryTypeList)) {
|
||||
throw new Forbidden("Sub-queries are forbidden in where.");
|
||||
}
|
||||
}
|
||||
|
||||
if ($attribute && $forbidComplexExpressions) {
|
||||
if (QueryUtil::isComplexExpression($attribute)) {
|
||||
throw new Forbidden("Complex expressions are forbidden in where.");
|
||||
}
|
||||
}
|
||||
|
||||
if ($attribute) {
|
||||
$argumentList = Util::getAllAttributesFromComplexExpression($attribute);
|
||||
|
||||
foreach ($argumentList as $argument) {
|
||||
$this->checkAttributeExistence($argument, $type);
|
||||
|
||||
if ($checkWherePermission) {
|
||||
$this->checkAttributePermission($argument, $type, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($type, $this->nestingTypeList) && is_array($value)) {
|
||||
foreach ($value as $subItem) {
|
||||
$this->checkItem(Item::fromRaw($subItem), $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function checkAttributeExistence(string $attribute, string $type): void
|
||||
{
|
||||
if (str_contains($attribute, '.')) {
|
||||
// @todo Check existence of foreign attributes.
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array($type, $this->linkTypeList)) {
|
||||
if (!$this->getSeed()->hasRelation($attribute)) {
|
||||
throw new BadRequest("Not existing relation '$attribute' in where.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->getSeed()->hasAttribute($attribute)) {
|
||||
throw new BadRequest("Not existing attribute '$attribute' in where.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function checkAttributePermission(string $attribute, string $type, mixed $value): 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.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array($type, $this->linkTypeList)) {
|
||||
$this->checkLink($type, $entityType, $attribute, $value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array($attribute, $this->acl->getScopeForbiddenAttributeList($entityType))) {
|
||||
throw new Forbidden("Forbidden attribute '$attribute' in where.");
|
||||
}
|
||||
}
|
||||
|
||||
private function getSeed(): Entity
|
||||
{
|
||||
$this->seed ??= $this->entityManager->getNewEntity($this->entityType);
|
||||
|
||||
return $this->seed;
|
||||
}
|
||||
|
||||
private function getRelationEntityType(Entity $entity, string $relation): mixed
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function checkLink(string $type, string $entityType, string $link, mixed $value): void
|
||||
{
|
||||
if (!$this->getSeed()->hasRelation($link)) {
|
||||
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 ($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 (!in_array($type, $this->linkWithIdsTypeList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_array($value)) {
|
||||
$value = [$value];
|
||||
}
|
||||
|
||||
foreach ($value as $it) {
|
||||
if (!is_string($it)) {
|
||||
throw new BadRequest("Bad where item. Non-string ID.");
|
||||
}
|
||||
}
|
||||
|
||||
// @todo Use the Select Builder instead. Check the result count equal the input IDs count.
|
||||
|
||||
foreach ($value as $id) {
|
||||
$entity = $this->entityManager->getEntityById($foreignEntityType, $id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new Forbidden("Record '$foreignEntityType' `$id` not found.");
|
||||
}
|
||||
|
||||
if (!$this->acl->checkEntityRead($entity)) {
|
||||
throw new Forbidden("No access to '$foreignEntityType' `$id`.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
application/Espo/Core/Select/Where/CheckerFactory.php
Normal file
60
application/Espo/Core/Select/Where/CheckerFactory.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where;
|
||||
|
||||
use Espo\Core\Acl\Exceptions\NotAvailable;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Acl\UserAclManagerProvider;
|
||||
use RuntimeException;
|
||||
|
||||
class CheckerFactory
|
||||
{
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private UserAclManagerProvider $userAclManagerProvider
|
||||
) {}
|
||||
|
||||
public function create(string $entityType, User $user): Checker
|
||||
{
|
||||
try {
|
||||
$acl = $this->userAclManagerProvider
|
||||
->get($user)
|
||||
->createUserAcl($user);
|
||||
} catch (NotAvailable $e) {
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->injectableFactory->createWith(Checker::class, [
|
||||
'entityType' => $entityType,
|
||||
'acl' => $acl,
|
||||
]);
|
||||
}
|
||||
}
|
||||
147
application/Espo/Core/Select/Where/Converter.php
Normal file
147
application/Espo/Core/Select/Where/Converter.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Select\Where\Item\Type;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
use Espo\ORM\Query\Part\Condition as Cond;
|
||||
use Espo\ORM\Query\Part\Expression as Expr;
|
||||
use Espo\ORM\Query\Part\Where\Comparison;
|
||||
use Espo\ORM\Query\Part\WhereClause;
|
||||
use Espo\ORM\Query\Part\WhereItem;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Converts a search where (passed from front-end) to a where clause (for ORM).
|
||||
*/
|
||||
class Converter
|
||||
{
|
||||
public function __construct(
|
||||
private ItemConverter $itemConverter,
|
||||
private Scanner $scanner
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function convert(QueryBuilder $queryBuilder, Item $item, ?Converter\Params $params = null): WhereItem
|
||||
{
|
||||
if ($params && $params->useSubQueryIfMany() && $this->hasRelatedMany($queryBuilder, $item)) {
|
||||
return $this->convertSubQuery($queryBuilder, $item);
|
||||
}
|
||||
|
||||
$whereClause = [];
|
||||
|
||||
foreach ($this->itemToList($item) as $subItemRaw) {
|
||||
try {
|
||||
$subItem = Item::fromRaw($subItemRaw);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new BadRequest($e->getMessage());
|
||||
}
|
||||
|
||||
$part = $this->processItem($queryBuilder, $subItem);
|
||||
|
||||
if ($part === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$whereClause[] = $part;
|
||||
}
|
||||
|
||||
$this->scanner->apply($queryBuilder, $item);
|
||||
|
||||
return WhereClause::fromRaw($whereClause);
|
||||
}
|
||||
|
||||
private function hasRelatedMany(QueryBuilder $queryBuilder, Item $item): bool
|
||||
{
|
||||
$entityType = $queryBuilder->build()->getFrom();
|
||||
|
||||
if (!$entityType) {
|
||||
throw new RuntimeException("No 'from' in queryBuilder.");
|
||||
}
|
||||
|
||||
return $this->scanner->hasRelatedMany($entityType, $item);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int|string, mixed>
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function itemToList(Item $item): array
|
||||
{
|
||||
if ($item->getType() !== Type::AND) {
|
||||
return [
|
||||
$item->getRaw(),
|
||||
];
|
||||
}
|
||||
|
||||
$list = $item->getValue();
|
||||
|
||||
if (!is_array($list)) {
|
||||
throw new BadRequest("Bad where item value.");
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int|string, mixed>
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function processItem(QueryBuilder $queryBuilder, Item $item): array
|
||||
{
|
||||
return $this->itemConverter->convert($queryBuilder, $item)->getRaw();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function convertSubQuery(QueryBuilder $queryBuilder, Item $item): Comparison
|
||||
{
|
||||
$entityType = $queryBuilder->build()->getFrom() ?? throw new RuntimeException();
|
||||
|
||||
$subQueryBuilder = QueryBuilder::create()
|
||||
->from($entityType)
|
||||
->select(Attribute::ID);
|
||||
|
||||
$subQueryBuilder->where(
|
||||
$this->convert($subQueryBuilder, $item)
|
||||
);
|
||||
|
||||
return Cond::in(
|
||||
Expr::column(Attribute::ID),
|
||||
$subQueryBuilder->build()
|
||||
);
|
||||
}
|
||||
}
|
||||
54
application/Espo/Core/Select/Where/Converter/Params.php
Normal file
54
application/Espo/Core/Select/Where/Converter/Params.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where\Converter;
|
||||
|
||||
/**
|
||||
* Where converter parameters.
|
||||
*
|
||||
* Immutable.
|
||||
* @since 9.0.0
|
||||
*/
|
||||
class Params
|
||||
{
|
||||
/**
|
||||
* @param bool $useSubQueryIfMany To use a sub-query if at least one has-many relation appears in a where clause.
|
||||
*/
|
||||
public function __construct(
|
||||
readonly private bool $useSubQueryIfMany = false,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* To use a sub-query if at least one has-many relation appears in a where clause.
|
||||
*/
|
||||
public function useSubQueryIfMany(): bool
|
||||
{
|
||||
return $this->useSubQueryIfMany;
|
||||
}
|
||||
}
|
||||
158
application/Espo/Core/Select/Where/ConverterFactory.php
Normal file
158
application/Espo/Core/Select/Where/ConverterFactory.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where;
|
||||
|
||||
use Espo\Core\Binding\Binder;
|
||||
use Espo\Core\Binding\BindingContainer;
|
||||
use Espo\Core\Binding\BindingData;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
|
||||
class ConverterFactory
|
||||
{
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private Metadata $metadata
|
||||
) {}
|
||||
|
||||
public function create(string $entityType, User $user): Converter
|
||||
{
|
||||
$dateTimeItemTransformer = $this->createDateTimeItemTransformer($entityType, $user);
|
||||
|
||||
$itemConverter = $this->createItemConverter($entityType, $user, $dateTimeItemTransformer);
|
||||
|
||||
$className = $this->getConverterClassName($entityType);
|
||||
|
||||
$bindingData = new BindingData();
|
||||
|
||||
$binder = new Binder($bindingData);
|
||||
$binder
|
||||
->bindInstance(User::class, $user);
|
||||
$binder
|
||||
->for($className)
|
||||
->bindValue('$entityType', $entityType)
|
||||
->bindInstance(ItemConverter::class, $itemConverter);
|
||||
|
||||
$bindingContainer = new BindingContainer($bindingData);
|
||||
|
||||
return $this->injectableFactory->createWithBinding($className, $bindingContainer);
|
||||
}
|
||||
|
||||
private function createDateTimeItemTransformer(string $entityType, User $user): DateTimeItemTransformer
|
||||
{
|
||||
$className = $this->getDateTimeItemTransformerClassName($entityType);
|
||||
|
||||
$bindingData = new BindingData();
|
||||
|
||||
$binder = new Binder($bindingData);
|
||||
$binder->bindInstance(User::class, $user);
|
||||
$binder
|
||||
->for($className)
|
||||
->bindValue('$entityType', $entityType);
|
||||
$binder
|
||||
->for(DefaultDateTimeItemTransformer::class)
|
||||
->bindValue('$entityType', $entityType);
|
||||
|
||||
$bindingContainer = new BindingContainer($bindingData);
|
||||
|
||||
return $this->injectableFactory->createWithBinding($className, $bindingContainer);
|
||||
}
|
||||
|
||||
private function createItemConverter(
|
||||
string $entityType,
|
||||
User $user,
|
||||
DateTimeItemTransformer $dateTimeItemTransformer
|
||||
): ItemConverter {
|
||||
|
||||
$className = $this->getItemConverterClassName($entityType);
|
||||
|
||||
$bindingData = new BindingData();
|
||||
|
||||
$binder = new Binder($bindingData);
|
||||
$binder
|
||||
->bindInstance(User::class, $user);
|
||||
$binder
|
||||
->for($className)
|
||||
->bindValue('$entityType', $entityType)
|
||||
->bindInstance(DateTimeItemTransformer::class, $dateTimeItemTransformer);
|
||||
$binder
|
||||
->for(ItemGeneralConverter::class)
|
||||
->bindValue('$entityType', $entityType)
|
||||
->bindInstance(DateTimeItemTransformer::class, $dateTimeItemTransformer);
|
||||
|
||||
$bindingContainer = new BindingContainer($bindingData);
|
||||
|
||||
return $this->injectableFactory->createWithBinding($className, $bindingContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<Converter>
|
||||
*/
|
||||
private function getConverterClassName(string $entityType): string
|
||||
{
|
||||
$className = $this->metadata->get(['selectDefs', $entityType, 'whereConverterClassName']);
|
||||
|
||||
if ($className) {
|
||||
return $className;
|
||||
}
|
||||
|
||||
return Converter::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<ItemGeneralConverter>
|
||||
*/
|
||||
private function getItemConverterClassName(string $entityType): string
|
||||
{
|
||||
$className = $this->metadata->get(['selectDefs', $entityType, 'whereItemConverterClassName']);
|
||||
|
||||
if ($className) {
|
||||
return $className;
|
||||
}
|
||||
|
||||
return ItemGeneralConverter::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<DateTimeItemTransformer>
|
||||
*/
|
||||
private function getDateTimeItemTransformerClassName(string $entityType): string
|
||||
{
|
||||
$className = $this->metadata
|
||||
->get(['selectDefs', $entityType, 'whereDateTimeItemTransformerClassName']);
|
||||
|
||||
if ($className) {
|
||||
return $className;
|
||||
}
|
||||
|
||||
return DefaultDateTimeItemTransformer::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
/**
|
||||
* Transforms date-time where item. Applies timezone. *
|
||||
* Implementing a custom transformer should not be considered future-proof.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface DateTimeItemTransformer
|
||||
{
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function transform(Item $item): Item;
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Select\Where\Item\Type;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use DateInterval;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
class DefaultDateTimeItemTransformer implements DateTimeItemTransformer
|
||||
{
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private Config\ApplicationConfig $applicationConfig,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function transform(Item $item): Item
|
||||
{
|
||||
$format = DateTimeUtil::SYSTEM_DATE_TIME_FORMAT;
|
||||
|
||||
$type = $item->getType();
|
||||
$value = $item->getValue();
|
||||
$attribute = $item->getAttribute();
|
||||
$data = $item->getData();
|
||||
|
||||
if (!$data instanceof Item\Data\DateTime) {
|
||||
throw new BadRequest("Bad where item.");
|
||||
}
|
||||
|
||||
$timeZone = $data->getTimeZone() ?? $this->applicationConfig->getTimeZone();
|
||||
|
||||
if (!$attribute) {
|
||||
throw new BadRequest("Bad datetime where item. Empty 'attribute'.");
|
||||
}
|
||||
|
||||
if (!$type) {
|
||||
throw new BadRequest("Bad datetime where item. Empty 'type'.");
|
||||
}
|
||||
|
||||
if (
|
||||
empty($value) &&
|
||||
in_array(
|
||||
$type,
|
||||
[
|
||||
Type::ON,
|
||||
Type::BEFORE,
|
||||
Type::AFTER,
|
||||
]
|
||||
)
|
||||
) {
|
||||
throw new BadRequest("Bad where item. Empty value.");
|
||||
}
|
||||
|
||||
$where = [
|
||||
'attribute' => $attribute,
|
||||
];
|
||||
|
||||
try {
|
||||
$dt = new DateTime('now', new DateTimeZone($timeZone));
|
||||
} catch (Exception) {
|
||||
throw new BadRequest("Bad timezone");
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case Type::TODAY:
|
||||
$where['type'] = Type::BETWEEN;
|
||||
|
||||
$dt->setTime(0, 0);
|
||||
|
||||
$dtTo = clone $dt;
|
||||
$dtTo->modify('+1 day -1 second');
|
||||
$dt->setTimezone(new DateTimeZone('UTC'));
|
||||
$dtTo->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$from = $dt->format($format);
|
||||
$to = $dtTo->format($format);
|
||||
|
||||
$where['value'] = [$from, $to];
|
||||
|
||||
break;
|
||||
|
||||
case Type::PAST:
|
||||
$where['type'] = Type::LESS_THAN_OR_EQUALS;
|
||||
|
||||
$dt->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$where['value'] = $dt->format($format);
|
||||
|
||||
break;
|
||||
|
||||
case Type::FUTURE:
|
||||
$where['type'] = Type::AFTER;
|
||||
|
||||
$dt->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$where['value'] = $dt->format($format);
|
||||
|
||||
break;
|
||||
|
||||
case Type::LAST_SEVEN_DAYS:
|
||||
$where['type'] = Type::BETWEEN;
|
||||
|
||||
$dtFrom = clone $dt;
|
||||
$dtTo = clone $dt;
|
||||
|
||||
$dtTo->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$to = $dtTo->format($format);
|
||||
|
||||
$dtFrom->modify('-7 day');
|
||||
$dtFrom->setTime(0, 0);
|
||||
$dtFrom->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$from = $dtFrom->format($format);
|
||||
|
||||
$where['value'] = [$from, $to];
|
||||
|
||||
break;
|
||||
|
||||
case Type::LAST_X_DAYS:
|
||||
$where['type'] = Type::BETWEEN;
|
||||
|
||||
$dtFrom = clone $dt;
|
||||
$dtTo = clone $dt;
|
||||
|
||||
$dtTo->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$to = $dtTo->format($format);
|
||||
|
||||
$number = strval(intval($value));
|
||||
|
||||
$dtFrom->modify('-'.$number.' day');
|
||||
$dtFrom->setTime(0, 0);
|
||||
$dtFrom->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$from = $dtFrom->format($format);
|
||||
|
||||
$where['value'] = [$from, $to];
|
||||
|
||||
break;
|
||||
|
||||
case Type::NEXT_X_DAYS:
|
||||
$where['type'] = Type::BETWEEN;
|
||||
|
||||
$dtTo = clone $dt;
|
||||
|
||||
$dt->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$from = $dt->format($format);
|
||||
|
||||
$number = strval(intval($value));
|
||||
|
||||
$dtTo->modify('+'.$number.' day');
|
||||
$dtTo->setTime(24, 59, 59);
|
||||
$dtTo->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$to = $dtTo->format($format);
|
||||
|
||||
$where['value'] = [$from, $to];
|
||||
|
||||
break;
|
||||
|
||||
case Type::OLDER_THAN_X_DAYS:
|
||||
$where['type'] = Type::BEFORE;
|
||||
|
||||
$number = strval(intval($value));
|
||||
|
||||
$dt->modify("-$number day");
|
||||
$dt->setTime(0, 0);
|
||||
$dt->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$where['value'] = $dt->format($format);
|
||||
|
||||
break;
|
||||
|
||||
case Type::AFTER_X_DAYS:
|
||||
$where['type'] = Type::GREATER_THAN_OR_EQUALS;
|
||||
|
||||
$number = strval(intval($value));
|
||||
|
||||
$dt->modify("+$number day");
|
||||
$dt->setTime(0, 0);
|
||||
$dt->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$where['value'] = $dt->format($format);
|
||||
|
||||
break;
|
||||
|
||||
case Type::ON:
|
||||
$where['type'] = Type::BETWEEN;
|
||||
|
||||
try {
|
||||
$dt = new DateTime($value, new DateTimeZone($timeZone));
|
||||
} catch (Exception) {
|
||||
throw new BadRequest("Bad date value or timezone.");
|
||||
}
|
||||
|
||||
$dtTo = clone $dt;
|
||||
|
||||
if (strlen($value) <= 10) {
|
||||
$dtTo->modify('+1 day -1 second');
|
||||
}
|
||||
|
||||
$dt->setTimezone(new DateTimeZone('UTC'));
|
||||
$dtTo->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$from = $dt->format($format);
|
||||
$to = $dtTo->format($format);
|
||||
|
||||
$where['value'] = [$from, $to];
|
||||
|
||||
break;
|
||||
|
||||
case Type::BEFORE:
|
||||
$where['type'] = Type::BEFORE;
|
||||
|
||||
try {
|
||||
$dt = new DateTime($value, new DateTimeZone($timeZone));
|
||||
} catch (Exception) {
|
||||
throw new BadRequest("Bad date value or timezone.");
|
||||
}
|
||||
|
||||
$dt->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$where['value'] = $dt->format($format);
|
||||
|
||||
break;
|
||||
|
||||
case Type::AFTER:
|
||||
$where['type'] = Type::AFTER;
|
||||
|
||||
try {
|
||||
$dt = new DateTime($value, new DateTimeZone($timeZone));
|
||||
} catch (Exception) {
|
||||
throw new BadRequest("Bad date value or timezone.");
|
||||
}
|
||||
|
||||
if (strlen($value) <= 10) {
|
||||
$dt->modify('+1 day -1 second');
|
||||
}
|
||||
|
||||
$dt->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$where['value'] = $dt->format($format);
|
||||
|
||||
break;
|
||||
|
||||
case Type::BETWEEN:
|
||||
$where['type'] = Type::BETWEEN;
|
||||
|
||||
if (!is_array($value) || count($value) < 2) {
|
||||
throw new BadRequest("Bad where item. Bad value.");
|
||||
}
|
||||
|
||||
try {
|
||||
$dt = new DateTime($value[0], new DateTimeZone($timeZone));
|
||||
} catch (Exception) {
|
||||
throw new BadRequest("Bad date value or timezone.");
|
||||
}
|
||||
|
||||
$dt->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$from = $dt->format($format);
|
||||
|
||||
try {
|
||||
$dt = new DateTime($value[1], new DateTimeZone($timeZone));
|
||||
} catch (Exception) {
|
||||
throw new BadRequest("Bad date value or timezone.");
|
||||
}
|
||||
|
||||
$dt->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
if (strlen($value[1]) <= 10) {
|
||||
$dt->modify('+1 day -1 second');
|
||||
}
|
||||
|
||||
$to = $dt->format($format);
|
||||
|
||||
$where['value'] = [$from, $to];
|
||||
|
||||
break;
|
||||
|
||||
case Type::CURRENT_MONTH:
|
||||
case Type::LAST_MONTH:
|
||||
case Type::NEXT_MONTH:
|
||||
$where['type'] = Type::BETWEEN;
|
||||
|
||||
$dtFrom = $dt->modify('first day of this month')->setTime(0, 0);
|
||||
|
||||
if ($type == Type::LAST_MONTH) {
|
||||
$dtFrom->modify('-1 month');
|
||||
} else if ($type == Type::NEXT_MONTH) {
|
||||
$dtFrom->modify('+1 month');
|
||||
}
|
||||
|
||||
$dtTo = clone $dtFrom;
|
||||
$dtTo->modify('+1 month')->modify('-1 second');
|
||||
|
||||
$dtFrom->setTimezone(new DateTimeZone('UTC'));
|
||||
$dtTo->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
|
||||
$where['value'] = [$dtFrom->format($format), $dtTo->format($format)];
|
||||
|
||||
break;
|
||||
|
||||
case Type::CURRENT_QUARTER:
|
||||
case Type::LAST_QUARTER:
|
||||
$where['type'] = Type::BETWEEN;
|
||||
|
||||
try {
|
||||
$dt = new DateTime('now', new DateTimeZone($timeZone));
|
||||
} catch (Exception) {
|
||||
throw new BadRequest("Bad timezone.");
|
||||
}
|
||||
|
||||
$quarter = ceil($dt->format('m') / 3);
|
||||
|
||||
$dtFrom = clone $dt;
|
||||
$dtFrom->modify('first day of January this year')->setTime(0, 0);
|
||||
|
||||
if ($type === Type::LAST_QUARTER) {
|
||||
$quarter--;
|
||||
|
||||
if ($quarter == 0) {
|
||||
$quarter = 4;
|
||||
$dtFrom->modify('-1 year');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$dtFrom->add(new DateInterval('P' . (($quarter - 1) * 3) . 'M'));
|
||||
} catch (Exception) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$dtTo = clone $dtFrom;
|
||||
$dtTo->add(new DateInterval('P3M'))->modify('-1 second');
|
||||
$dtFrom->setTimezone(new DateTimeZone('UTC'));
|
||||
$dtTo->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$where['value'] = [
|
||||
$dtFrom->format($format),
|
||||
$dtTo->format($format),
|
||||
];
|
||||
|
||||
break;
|
||||
|
||||
case Type::CURRENT_YEAR:
|
||||
case Type::LAST_YEAR:
|
||||
$where['type'] = Type::BETWEEN;
|
||||
|
||||
try {
|
||||
$dtFrom = new DateTime('now', new DateTimeZone($timeZone));
|
||||
} catch (Exception) {
|
||||
throw new BadRequest("Bad timezone.");
|
||||
}
|
||||
|
||||
$dtFrom->modify('first day of January this year')->setTime(0, 0);
|
||||
|
||||
if ($type == Type::LAST_YEAR) {
|
||||
$dtFrom->modify('-1 year');
|
||||
}
|
||||
|
||||
$dtTo = clone $dtFrom;
|
||||
$dtTo = $dtTo->modify('+1 year')->modify('-1 second');
|
||||
$dtFrom->setTimezone(new DateTimeZone('UTC'));
|
||||
$dtTo->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$where['value'] = [
|
||||
$dtFrom->format($format),
|
||||
$dtTo->format($format),
|
||||
];
|
||||
|
||||
break;
|
||||
|
||||
case Type::CURRENT_FISCAL_YEAR:
|
||||
case Type::LAST_FISCAL_YEAR:
|
||||
$where['type'] = Type::BETWEEN;
|
||||
|
||||
try {
|
||||
$dtToday = new DateTime('now', new DateTimeZone($timeZone));
|
||||
} catch (Exception) {
|
||||
throw new BadRequest("Bad timezone.");
|
||||
}
|
||||
|
||||
$dt = clone $dtToday;
|
||||
$fiscalYearShift = $this->config->get('fiscalYearShift', 0);
|
||||
|
||||
$dt
|
||||
->modify('first day of January this year')
|
||||
->modify('+' . $fiscalYearShift . ' months')
|
||||
->setTime(0, 0);
|
||||
|
||||
if (intval($dtToday->format('m')) < $fiscalYearShift + 1) {
|
||||
$dt->modify('-1 year');
|
||||
}
|
||||
|
||||
if ($type === Type::LAST_FISCAL_YEAR) {
|
||||
$dt->modify('-1 year');
|
||||
}
|
||||
|
||||
$dtFrom = clone $dt;
|
||||
$dtTo = clone $dt;
|
||||
$dtTo = $dtTo->modify('+1 year')->modify('-1 second');
|
||||
|
||||
$dtFrom->setTimezone(new DateTimeZone('UTC'));
|
||||
$dtTo->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$where['value'] = [
|
||||
$dtFrom->format($format),
|
||||
$dtTo->format($format),
|
||||
];
|
||||
|
||||
break;
|
||||
|
||||
case Type::CURRENT_FISCAL_QUARTER:
|
||||
case Type::LAST_FISCAL_QUARTER:
|
||||
$where['type'] = Type::BETWEEN;
|
||||
|
||||
try {
|
||||
$dtToday = new DateTime('now', new DateTimeZone($timeZone));
|
||||
} catch (Exception) {
|
||||
throw new BadRequest("Bad timezone.");
|
||||
}
|
||||
|
||||
$dt = clone $dtToday;
|
||||
|
||||
$fiscalYearShift = $this->config->get('fiscalYearShift', 0);
|
||||
|
||||
$dt
|
||||
->modify('first day of January this year')
|
||||
->modify('+' . $fiscalYearShift . ' months')
|
||||
->setTime(0, 0);
|
||||
|
||||
$month = intval($dtToday->format('m'));
|
||||
|
||||
$quarterShift = floor(($month - $fiscalYearShift - 1) / 3);
|
||||
|
||||
if ($quarterShift) {
|
||||
if ($quarterShift >= 0) {
|
||||
try {
|
||||
$dt->add(new DateInterval('P' . ($quarterShift * 3) . 'M'));
|
||||
} catch (Exception) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
} else {
|
||||
$quarterShift *= -1;
|
||||
|
||||
try {
|
||||
$dt->sub(new DateInterval('P' . ($quarterShift * 3) . 'M'));
|
||||
} catch (Exception) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($type === Type::LAST_FISCAL_QUARTER) {
|
||||
$dt->modify('-3 months');
|
||||
}
|
||||
|
||||
$dtFrom = clone $dt;
|
||||
$dtTo = clone $dt;
|
||||
$dtTo = $dtTo->modify('+3 months')->modify('-1 second');
|
||||
|
||||
$dtFrom->setTimezone(new DateTimeZone('UTC'));
|
||||
$dtTo->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
$where['value'] = [
|
||||
$dtFrom->format($format),
|
||||
$dtTo->format($format),
|
||||
];
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$where['type'] = $type;
|
||||
$where['value'] = $value;
|
||||
}
|
||||
|
||||
return Item::fromRaw($where);
|
||||
}
|
||||
}
|
||||
253
application/Espo/Core/Select/Where/Item.php
Normal file
253
application/Espo/Core/Select/Where/Item.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where;
|
||||
|
||||
use Espo\Core\Select\Where\Item\Data;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* A where item.
|
||||
*
|
||||
* Immutable.
|
||||
*/
|
||||
class Item
|
||||
{
|
||||
public const TYPE_AND = Item\Type::AND;
|
||||
public const TYPE_OR = Item\Type::OR;
|
||||
|
||||
private ?string $attribute = null;
|
||||
private mixed $value = null;
|
||||
private ?Data $data = null;
|
||||
|
||||
/** @var string[] */
|
||||
private $noAttributeTypeList = [
|
||||
Item\Type::AND,
|
||||
Item\Type::OR,
|
||||
Item\Type::NOT,
|
||||
Item\Type::SUBQUERY_IN,
|
||||
Item\Type::SUBQUERY_NOT_IN,
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private $withNestedItemsTypeList = [
|
||||
Item\Type::AND,
|
||||
Item\Type::OR,
|
||||
];
|
||||
|
||||
private function __construct(private string $type)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
* @internal
|
||||
*/
|
||||
public static function fromRaw(array $params): self
|
||||
{
|
||||
$type = $params['type'] ?? null;
|
||||
|
||||
if (!$type) {
|
||||
throw new InvalidArgumentException("No 'type' in where item.");
|
||||
}
|
||||
|
||||
$obj = new self($type);
|
||||
|
||||
$obj->attribute = $params['attribute'] ?? $params['field'] ?? null;
|
||||
$obj->value = $params['value'] ?? null;
|
||||
|
||||
if ($params['dateTime'] ?? false) {
|
||||
$obj->data = Data\DateTime
|
||||
::create()
|
||||
->withTimeZone($params['timeZone'] ?? null);
|
||||
} else if ($params['date'] ?? null) {
|
||||
$obj->data = Data\Date
|
||||
::create()
|
||||
->withTimeZone($params['timeZone'] ?? null);
|
||||
}
|
||||
|
||||
unset($params['field']);
|
||||
unset($params['dateTime']);
|
||||
unset($params['date']);
|
||||
unset($params['timeZone']);
|
||||
|
||||
foreach (array_keys($params) as $key) {
|
||||
if (!property_exists($obj, $key)) {
|
||||
throw new InvalidArgumentException("Unknown parameter '$key'.");
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!$obj->attribute &&
|
||||
!in_array($obj->type, $obj->noAttributeTypeList)
|
||||
) {
|
||||
throw new InvalidArgumentException("No 'attribute' in where item.");
|
||||
}
|
||||
|
||||
if (in_array($obj->type, $obj->withNestedItemsTypeList)) {
|
||||
$obj->value = $obj->value ?? [];
|
||||
|
||||
if (
|
||||
!is_array($obj->value) ||
|
||||
count($obj->value) && array_keys($obj->value) !== range(0, count($obj->value) - 1)
|
||||
) {
|
||||
throw new InvalidArgumentException("Bad 'value'.");
|
||||
}
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array<int|string, mixed>> $paramList
|
||||
* {@internal}
|
||||
*/
|
||||
public static function fromRawAndGroup(array $paramList): self
|
||||
{
|
||||
return self::fromRaw([
|
||||
'type' => Item\Type::AND,
|
||||
'value' => $paramList,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* type: string,
|
||||
* value: mixed,
|
||||
* attribute?: string,
|
||||
* dateTime?: bool,
|
||||
* timeZone?: string,
|
||||
* }
|
||||
* {@internal}
|
||||
*/
|
||||
public function getRaw(): array
|
||||
{
|
||||
$type = $this->type;
|
||||
|
||||
$raw = [
|
||||
'type' => $type,
|
||||
'value' => $this->value,
|
||||
];
|
||||
|
||||
if ($this->attribute) {
|
||||
$raw['attribute'] = $this->attribute;
|
||||
}
|
||||
|
||||
if ($this->data instanceof Data\DateTime || $this->data instanceof Data\Date) {
|
||||
if ($this->data instanceof Data\DateTime) {
|
||||
$raw['dateTime'] = true;
|
||||
}
|
||||
|
||||
$timeZone = $this->data->getTimeZone();
|
||||
|
||||
if ($timeZone) {
|
||||
$raw['timeZone'] = $timeZone;
|
||||
}
|
||||
}
|
||||
|
||||
return $raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a type;
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an attribute.
|
||||
*/
|
||||
public function getAttribute(): ?string
|
||||
{
|
||||
return $this->attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nested where items (for 'and', 'or' types).
|
||||
*
|
||||
* @return Item[]
|
||||
* @throws RuntimeException If a type does not support nested items.
|
||||
*/
|
||||
public function getItemList(): array
|
||||
{
|
||||
if (!in_array($this->type, $this->withNestedItemsTypeList)) {
|
||||
throw new RuntimeException("Nested items not supported for '$this->type' type.");
|
||||
}
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach ($this->value as $raw) {
|
||||
$list[] = Item::fromRaw($raw);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a data-object.
|
||||
*/
|
||||
public function getData(): ?Data
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder.
|
||||
*/
|
||||
public static function createBuilder(): ItemBuilder
|
||||
{
|
||||
return new ItemBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone with data.
|
||||
*
|
||||
* {@internal}
|
||||
*/
|
||||
public function withData(?Data $data): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->data = $data;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
32
application/Espo/Core/Select/Where/Item/Data.php
Normal file
32
application/Espo/Core/Select/Where/Item/Data.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where\Item;
|
||||
|
||||
interface Data {}
|
||||
55
application/Espo/Core/Select/Where/Item/Data/Date.php
Normal file
55
application/Espo/Core/Select/Where/Item/Data/Date.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where\Item\Data;
|
||||
|
||||
use Espo\Core\Select\Where\Item\Data;
|
||||
|
||||
class Date implements Data
|
||||
{
|
||||
private ?string $timeZone = null;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function withTimeZone(?string $timeZone): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->timeZone = $timeZone;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function getTimeZone(): ?string
|
||||
{
|
||||
return $this->timeZone;
|
||||
}
|
||||
}
|
||||
55
application/Espo/Core/Select/Where/Item/Data/DateTime.php
Normal file
55
application/Espo/Core/Select/Where/Item/Data/DateTime.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where\Item\Data;
|
||||
|
||||
use Espo\Core\Select\Where\Item\Data;
|
||||
|
||||
class DateTime implements Data
|
||||
{
|
||||
private ?string $timeZone = null;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function withTimeZone(?string $timeZone): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->timeZone = $timeZone;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function getTimeZone(): ?string
|
||||
{
|
||||
return $this->timeZone;
|
||||
}
|
||||
}
|
||||
95
application/Espo/Core/Select/Where/Item/Type.php
Normal file
95
application/Espo/Core/Select/Where/Item/Type.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where\Item;
|
||||
|
||||
class Type
|
||||
{
|
||||
public const AND = 'and';
|
||||
public const OR = 'or';
|
||||
public const NOT = 'not';
|
||||
public const SUBQUERY_NOT_IN = 'subQueryNotIn';
|
||||
public const SUBQUERY_IN = 'subQueryIn';
|
||||
public const EXPRESSION = 'expression';
|
||||
public const IN = 'in';
|
||||
public const NOT_IN = 'notIn';
|
||||
public const EQUALS = 'equals';
|
||||
public const NOT_EQUALS = 'notEquals';
|
||||
public const ON = 'on';
|
||||
public const NOT_ON = 'notOn';
|
||||
public const LIKE = 'like';
|
||||
public const NOT_LIKE = 'notLike';
|
||||
public const STARTS_WITH = 'startsWith';
|
||||
public const ENDS_WITH = 'endsWith';
|
||||
public const CONTAINS = 'contains';
|
||||
public const NOT_CONTAINS = 'notContains';
|
||||
public const GREATER_THAN = 'greaterThan';
|
||||
public const LESS_THAN = 'lessThan';
|
||||
public const GREATER_THAN_OR_EQUALS = 'greaterThanOrEquals';
|
||||
public const LESS_THAN_OR_EQUALS = 'lessThanOrEquals';
|
||||
public const AFTER = 'after';
|
||||
public const BEFORE = 'before';
|
||||
public const BETWEEN = 'between';
|
||||
public const EVER = 'ever';
|
||||
public const ANY = 'any';
|
||||
public const NONE = 'none';
|
||||
public const IS_NULL = 'isNull';
|
||||
public const IS_NOT_NULL = 'isNotNull';
|
||||
public const IS_TRUE = 'isTrue';
|
||||
public const IS_FALSE = 'isFalse';
|
||||
public const TODAY = 'today';
|
||||
public const PAST = 'past';
|
||||
public const FUTURE = 'future';
|
||||
public const LAST_SEVEN_DAYS = 'lastSevenDays';
|
||||
public const LAST_X_DAYS = 'lastXDays';
|
||||
public const NEXT_X_DAYS = 'nextXDays';
|
||||
public const OLDER_THAN_X_DAYS = 'olderThanXDays';
|
||||
public const AFTER_X_DAYS = 'afterXDays';
|
||||
public const CURRENT_MONTH = 'currentMonth';
|
||||
public const NEXT_MONTH = 'nextMonth';
|
||||
public const LAST_MONTH = 'lastMonth';
|
||||
public const CURRENT_QUARTER = 'currentQuarter';
|
||||
public const LAST_QUARTER = 'lastQuarter';
|
||||
public const CURRENT_YEAR = 'currentYear';
|
||||
public const LAST_YEAR = 'lastYear';
|
||||
public const CURRENT_FISCAL_YEAR = 'currentFiscalYear';
|
||||
public const LAST_FISCAL_YEAR = 'lastFiscalYear';
|
||||
public const CURRENT_FISCAL_QUARTER = 'currentFiscalQuarter';
|
||||
public const LAST_FISCAL_QUARTER = 'lastFiscalQuarter';
|
||||
public const ARRAY_ANY_OF = 'arrayAnyOf';
|
||||
public const ARRAY_NONE_OF = 'arrayNoneOf';
|
||||
public const ARRAY_ALL_OF = 'arrayAllOf';
|
||||
public const ARRAY_IS_EMPTY = 'arrayIsEmpty';
|
||||
public const ARRAY_IS_NOT_EMPTY = 'arrayIsNotEmpty';
|
||||
public const IS_LINKED_WITH = 'linkedWith';
|
||||
public const IS_NOT_LINKED_WITH = 'notLinkedWith';
|
||||
public const IS_LINKED_WITH_ALL = 'linkedWithAll';
|
||||
public const IS_LINKED_WITH_ANY = 'isLinked';
|
||||
public const IS_LINKED_WITH_NONE = 'isNotLinked';
|
||||
}
|
||||
124
application/Espo/Core/Select/Where/ItemBuilder.php
Normal file
124
application/Espo/Core/Select/Where/ItemBuilder.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where;
|
||||
|
||||
use Espo\Core\Select\Where\Item\Data;
|
||||
|
||||
/**
|
||||
* A where-item builder.
|
||||
*/
|
||||
class ItemBuilder
|
||||
{
|
||||
private ?string $type = null;
|
||||
private ?string $attribute = null;
|
||||
/** @var mixed */
|
||||
private $value = null;
|
||||
private ?Data $data = null;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a type.
|
||||
*
|
||||
* @param (Item\Type::*)|string $type
|
||||
* @return $this
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function setType(string $type): self
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function setValue($value): self
|
||||
{
|
||||
$this->value = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an attribute.
|
||||
*/
|
||||
public function setAttribute(?string $attribute): self
|
||||
{
|
||||
$this->attribute = $attribute;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data.
|
||||
*/
|
||||
public function setData(?Data $data): self
|
||||
{
|
||||
$this->data = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set nested where item list.
|
||||
*
|
||||
* @param Item[] $itemList
|
||||
* @return self
|
||||
*/
|
||||
public function setItemList(array $itemList): self
|
||||
{
|
||||
$this->value = array_map(
|
||||
function (Item $item): array {
|
||||
return $item->getRaw();
|
||||
},
|
||||
$itemList
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function build(): Item
|
||||
{
|
||||
return Item
|
||||
::fromRaw([
|
||||
'type' => $this->type,
|
||||
'attribute' => $this->attribute,
|
||||
'value' => $this->value,
|
||||
])
|
||||
->withData($this->data);
|
||||
}
|
||||
}
|
||||
45
application/Espo/Core/Select/Where/ItemConverter.php
Normal file
45
application/Espo/Core/Select/Where/ItemConverter.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
/**
|
||||
* Converts a where item to a where clause item (for ORM).
|
||||
*/
|
||||
interface ItemConverter
|
||||
{
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function convert(QueryBuilder $queryBuilder, Item $item): WhereClauseItem;
|
||||
}
|
||||
118
application/Espo/Core/Select/Where/ItemConverterFactory.php
Normal file
118
application/Espo/Core/Select/Where/ItemConverterFactory.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where;
|
||||
|
||||
use Espo\Core\Binding\Binder;
|
||||
use Espo\Core\Binding\BindingContainer;
|
||||
use Espo\Core\Binding\BindingData;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
use Espo\Entities\User;
|
||||
use RuntimeException;
|
||||
|
||||
class ItemConverterFactory
|
||||
{
|
||||
public function __construct(private InjectableFactory $injectableFactory, private Metadata $metadata)
|
||||
{}
|
||||
|
||||
public function hasForType(string $type): bool
|
||||
{
|
||||
return (bool) $this->getClassNameForType($type);
|
||||
}
|
||||
|
||||
public function createForType(string $type, string $entityType, User $user): ItemConverter
|
||||
{
|
||||
$className = $this->getClassNameForType($type);
|
||||
|
||||
if (!$className) {
|
||||
throw new RuntimeException("Where item converter class name is not defined.");
|
||||
}
|
||||
|
||||
$bindingData = new BindingData();
|
||||
|
||||
$binder = new Binder($bindingData);
|
||||
|
||||
$binder
|
||||
->bindInstance(User::class, $user);
|
||||
|
||||
$binder
|
||||
->for($className)
|
||||
->bindValue('$entityType', $entityType);
|
||||
|
||||
$bindingContainer = new BindingContainer($bindingData);
|
||||
|
||||
return $this->injectableFactory->createWithBinding($className, $bindingContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?class-string<ItemConverter>
|
||||
*/
|
||||
protected function getClassNameForType(string $type): ?string
|
||||
{
|
||||
return $this->metadata->get(['app', 'select', 'whereItemConverterClassNameMap', $type]);
|
||||
}
|
||||
|
||||
public function has(string $entityType, string $attribute, string $type): bool
|
||||
{
|
||||
return (bool) $this->getClassName($entityType, $attribute, $type);
|
||||
}
|
||||
|
||||
public function create(string $entityType, string $attribute, string $type, User $user): ItemConverter
|
||||
{
|
||||
$className = $this->getClassName($entityType, $attribute, $type);
|
||||
|
||||
if (!$className) {
|
||||
throw new RuntimeException("Where item converter class name is not defined.");
|
||||
}
|
||||
|
||||
$bindingData = new BindingData();
|
||||
|
||||
$binder = new Binder($bindingData);
|
||||
$binder
|
||||
->bindInstance(User::class, $user);
|
||||
$binder
|
||||
->for($className)
|
||||
->bindValue('$entityType', $entityType);
|
||||
|
||||
$bindingContainer = new BindingContainer($bindingData);
|
||||
|
||||
return $this->injectableFactory->createWithBinding($className, $bindingContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?class-string<ItemConverter>
|
||||
*/
|
||||
protected function getClassName(string $entityType, string $attribute, string $type): ?string
|
||||
{
|
||||
return $this->metadata
|
||||
->get(['selectDefs', $entityType, 'whereItemConverterClassNameMap', $attribute . '_' . $type]);
|
||||
}
|
||||
}
|
||||
116
application/Espo/Core/Select/Where/ItemConverters/InCategory.php
Normal file
116
application/Espo/Core/Select/Where/ItemConverters/InCategory.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where\ItemConverters;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Select\Where\Item;
|
||||
use Espo\Core\Select\Where\ItemConverter;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Query\Part\Condition as Cond;
|
||||
use Espo\ORM\Query\Part\Expression as Expr;
|
||||
use Espo\ORM\Query\Part\WhereClause;
|
||||
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class InCategory implements ItemConverter
|
||||
{
|
||||
public function __construct(
|
||||
private string $entityType,
|
||||
private Defs $ormDefs
|
||||
) {}
|
||||
|
||||
public function convert(QueryBuilder $queryBuilder, Item $item): WhereClauseItem
|
||||
{
|
||||
$link = $item->getAttribute();
|
||||
$value = $item->getValue();
|
||||
|
||||
if (!$link) {
|
||||
throw new BadRequest("No attribute.");
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return WhereClause::create();
|
||||
}
|
||||
|
||||
if (!is_array($value)) {
|
||||
$value = [$value];
|
||||
}
|
||||
|
||||
foreach ($value as $it) {
|
||||
if (!is_string($it) && !is_int($it)) {
|
||||
throw new BadRequest("Bad where item. Bad array item.");
|
||||
}
|
||||
}
|
||||
|
||||
$entityDefs = $this->ormDefs->getEntity($this->entityType);
|
||||
|
||||
if (!$entityDefs->hasRelation($link)) {
|
||||
throw new BadRequest("Not existing '$link' in where item.");
|
||||
}
|
||||
|
||||
$defs = $entityDefs->getRelation($link);
|
||||
|
||||
$path = lcfirst($defs->getForeignEntityType()) . 'Path';
|
||||
|
||||
if ($defs->getType() === Entity::MANY_MANY) {
|
||||
$middle = ucfirst($defs->getRelationshipName());
|
||||
|
||||
return Cond::in(
|
||||
Expr::column('id'),
|
||||
QueryBuilder::create()
|
||||
->from($middle, 'm')
|
||||
->select($defs->getMidKey())
|
||||
->join(
|
||||
ucfirst($path),
|
||||
$path,
|
||||
["$path.descendorId:" => "m.{$defs->getForeignMidKey()}"]
|
||||
)
|
||||
->where(["$path.ascendorId" => $value])
|
||||
->build()
|
||||
);
|
||||
}
|
||||
|
||||
if ($defs->getType() === Entity::BELONGS_TO) {
|
||||
$queryBuilder->join(
|
||||
ucfirst($path),
|
||||
$path,
|
||||
["$path.descendorId:" => $defs->getKey()]
|
||||
);
|
||||
|
||||
return WhereClause::fromRaw(["$path.ascendorId" => $value]);
|
||||
}
|
||||
|
||||
throw new BadRequest("Not supported link '$link' in where item.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where\ItemConverters;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Select\Where\Item;
|
||||
use Espo\Core\Select\Where\ItemConverter;
|
||||
use Espo\Entities\Team;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Query\Part\Condition;
|
||||
use Espo\ORM\Query\Part\Expression;
|
||||
use Espo\ORM\Query\Part\WhereClause;
|
||||
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class IsUserFromTeams implements ItemConverter
|
||||
{
|
||||
public function __construct(
|
||||
private string $entityType,
|
||||
private Defs $ormDefs,
|
||||
) {}
|
||||
|
||||
public function convert(QueryBuilder $queryBuilder, Item $item): WhereClauseItem
|
||||
{
|
||||
$link = $item->getAttribute();
|
||||
$value = $item->getValue();
|
||||
|
||||
if (!$link) {
|
||||
throw new BadRequest("No attribute.");
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return WhereClause::create();
|
||||
}
|
||||
|
||||
if (!is_array($value)) {
|
||||
$value = [$value];
|
||||
}
|
||||
|
||||
foreach ($value as $it) {
|
||||
if (!is_string($it) && !is_int($it)) {
|
||||
throw new BadRequest("Bad where item. Bad array item.");
|
||||
}
|
||||
}
|
||||
|
||||
$entityDefs = $this->ormDefs->getEntity($this->entityType);
|
||||
|
||||
if (!$entityDefs->hasRelation($link)) {
|
||||
throw new BadRequest("Not existing '$link' in where item.");
|
||||
}
|
||||
|
||||
$defs = $entityDefs->getRelation($link);
|
||||
|
||||
$relationType = $defs->getType();
|
||||
$entityType = $defs->getForeignEntityType();
|
||||
|
||||
if ($entityType !== User::ENTITY_TYPE) {
|
||||
throw new BadRequest("Not supported link '$link' in where item.");
|
||||
}
|
||||
|
||||
if ($relationType === Entity::BELONGS_TO) {
|
||||
return Condition::in(
|
||||
Expression::column($defs->getKey()),
|
||||
SelectBuilder::create()
|
||||
->from(Team::RELATIONSHIP_TEAM_USER, 'sq')
|
||||
->select('userId')
|
||||
->where(['teamId' => $value])
|
||||
->build()
|
||||
);
|
||||
}
|
||||
|
||||
throw new BadRequest("Not supported link '$link' in where item.");
|
||||
}
|
||||
}
|
||||
1713
application/Espo/Core/Select/Where/ItemGeneralConverter.php
Normal file
1713
application/Espo/Core/Select/Where/ItemGeneralConverter.php
Normal file
File diff suppressed because it is too large
Load Diff
84
application/Espo/Core/Select/Where/Params.php
Normal file
84
application/Espo/Core/Select/Where/Params.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Where parameters.
|
||||
*
|
||||
* Immutable.
|
||||
*/
|
||||
class Params
|
||||
{
|
||||
private bool $applyPermissionCheck = false;
|
||||
private bool $forbidComplexExpressions = false;
|
||||
|
||||
private function __construct()
|
||||
{}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* applyPermissionCheck?: bool,
|
||||
* forbidComplexExpressions?: bool,
|
||||
* } $params
|
||||
*/
|
||||
public static function fromAssoc(array $params): self
|
||||
{
|
||||
$object = new self();
|
||||
|
||||
$object->applyPermissionCheck = $params['applyPermissionCheck'] ?? false;
|
||||
$object->forbidComplexExpressions = $params['forbidComplexExpressions'] ?? false;
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
if (!property_exists($object, $key)) {
|
||||
throw new InvalidArgumentException("Unknown parameter '$key'.");
|
||||
}
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply permission check.
|
||||
*/
|
||||
public function applyPermissionCheck(): bool
|
||||
{
|
||||
return $this->applyPermissionCheck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forbid complex expressions.
|
||||
*/
|
||||
public function forbidComplexExpressions(): bool
|
||||
{
|
||||
return $this->forbidComplexExpressions;
|
||||
}
|
||||
}
|
||||
233
application/Espo/Core/Select/Where/Scanner.php
Normal file
233
application/Espo/Core/Select/Where/Scanner.php
Normal file
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Select\Where;
|
||||
|
||||
use Espo\Core\Select\Where\Item\Type;
|
||||
use Espo\ORM\Defs\Params\AttributeParam;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\BaseEntity;
|
||||
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
|
||||
use Espo\ORM\QueryComposer\Util as QueryComposerUtil;
|
||||
use Espo\ORM\Type\RelationType;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Scans where items.
|
||||
*/
|
||||
class Scanner
|
||||
{
|
||||
/** @var array<string, Entity> */
|
||||
private array $seedHash = [];
|
||||
|
||||
/** @var string[] */
|
||||
private $nestingTypeList = [
|
||||
Type::OR,
|
||||
Type::AND,
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private array $subQueryTypeList = [
|
||||
Type::SUBQUERY_IN,
|
||||
Type::SUBQUERY_NOT_IN,
|
||||
Type::NOT,
|
||||
];
|
||||
|
||||
public function __construct(private EntityManager $entityManager)
|
||||
{}
|
||||
|
||||
/**
|
||||
* Check whether at least one has-many link appears in the where-clause.
|
||||
*
|
||||
* @since 9.0.0
|
||||
*/
|
||||
public function hasRelatedMany(string $entityType, Item $item): bool
|
||||
{
|
||||
$type = $item->getType();
|
||||
$attribute = $item->getAttribute();
|
||||
|
||||
if (in_array($type, $this->subQueryTypeList)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (in_array($type, $this->nestingTypeList)) {
|
||||
foreach ($item->getItemList() as $subItem) {
|
||||
if ($this->hasRelatedMany($entityType, $subItem)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$attribute) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$seed = $this->getSeed($entityType);
|
||||
|
||||
|
||||
foreach (QueryComposerUtil::getAllAttributesFromComplexExpression($attribute) as $expr) {
|
||||
if (!str_contains($expr, '.')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$link,] = explode('.', $expr);
|
||||
|
||||
if (!$seed->hasRelation($link)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isMany = in_array($seed->getRelationType($link), [
|
||||
RelationType::HAS_MANY,
|
||||
RelationType::MANY_MANY,
|
||||
RelationType::HAS_CHILDREN,
|
||||
]);
|
||||
|
||||
if ($isMany) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply needed joins to a query builder.
|
||||
*/
|
||||
public function apply(QueryBuilder $queryBuilder, Item $item): void
|
||||
{
|
||||
$entityType = $queryBuilder->build()->getFrom();
|
||||
|
||||
if (!$entityType) {
|
||||
throw new RuntimeException("No entity type.");
|
||||
}
|
||||
|
||||
$this->applyLeftJoinsFromItem($queryBuilder, $item, $entityType);
|
||||
}
|
||||
|
||||
private function applyLeftJoinsFromItem(QueryBuilder $queryBuilder, Item $item, string $entityType): void
|
||||
{
|
||||
$type = $item->getType();
|
||||
$value = $item->getValue();
|
||||
$attribute = $item->getAttribute();
|
||||
|
||||
if (in_array($type, $this->subQueryTypeList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array($type, $this->nestingTypeList)) {
|
||||
if (!is_array($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($value as $subItem) {
|
||||
$this->applyLeftJoinsFromItem($queryBuilder, Item::fromRaw($subItem), $entityType);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$attribute) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->applyLeftJoinsFromAttribute($queryBuilder, $attribute, $entityType);
|
||||
}
|
||||
|
||||
private function applyLeftJoinsFromAttribute(
|
||||
QueryBuilder $queryBuilder,
|
||||
string $attribute,
|
||||
string $entityType
|
||||
): void {
|
||||
|
||||
if (str_contains($attribute, ':')) {
|
||||
$argumentList = QueryComposerUtil::getAllAttributesFromComplexExpression($attribute);
|
||||
|
||||
foreach ($argumentList as $argument) {
|
||||
$this->applyLeftJoinsFromAttribute($queryBuilder, $argument, $entityType);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$seed = $this->getSeed($entityType);
|
||||
|
||||
if (str_contains($attribute, '.')) {
|
||||
[$link,] = explode('.', $attribute);
|
||||
|
||||
if ($seed->hasRelation($link)) {
|
||||
$queryBuilder->leftJoin($link);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$attributeType = $seed->getAttributeType($attribute);
|
||||
|
||||
if ($attributeType === Entity::FOREIGN) {
|
||||
$relation = $this->getAttributeParam($seed, $attribute, AttributeParam::RELATION);
|
||||
|
||||
if ($relation) {
|
||||
$queryBuilder->leftJoin($relation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getSeed(string $entityType): Entity
|
||||
{
|
||||
if (!isset($this->seedHash[$entityType])) {
|
||||
$this->seedHash[$entityType] = $this->entityManager->getNewEntity($entityType);
|
||||
}
|
||||
|
||||
return $this->seedHash[$entityType];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @noinspection PhpSameParameterValueInspection
|
||||
*/
|
||||
private function getAttributeParam(Entity $entity, string $attribute, string $param)
|
||||
{
|
||||
if ($entity instanceof BaseEntity) {
|
||||
return $entity->getAttributeParam($attribute, $param);
|
||||
}
|
||||
|
||||
$entityDefs = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entity->getEntityType());
|
||||
|
||||
if (!$entityDefs->hasAttribute($attribute)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $entityDefs->getAttribute($attribute)->getParam($param);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user