Initial commit
This commit is contained in:
381
application/Espo/Core/Acl.php
Normal file
381
application/Espo/Core/Acl.php
Normal file
@@ -0,0 +1,381 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\Acl\Exceptions\NotImplemented;
|
||||
use Espo\Core\Acl\GlobalRestriction;
|
||||
use Espo\Core\Acl\Permission;
|
||||
use Espo\Core\Acl\Table;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* A wrapper for `AclManager` for a current user. A central access point for access checking.
|
||||
*/
|
||||
class Acl
|
||||
{
|
||||
public function __construct(
|
||||
protected AclManager $aclManager,
|
||||
protected User $user
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get a full access data map.
|
||||
*/
|
||||
public function getMapData(): stdClass
|
||||
{
|
||||
return $this->aclManager->getMapData($this->user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an access level for a specific scope and action.
|
||||
*
|
||||
* @param Table::ACTION_* $action
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function getLevel(string $scope, string $action): string
|
||||
{
|
||||
return $this->aclManager->getLevel($this->user, $scope, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a permission. E.g. 'assignment' permission.
|
||||
*/
|
||||
public function getPermissionLevel(string $permission): string
|
||||
{
|
||||
return $this->aclManager->getPermissionLevel($this->user, $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether there's no 'read' access for a specific scope.
|
||||
*/
|
||||
public function checkReadNo(string $scope): bool
|
||||
{
|
||||
return $this->aclManager->checkReadNo($this->user, $scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether 'read' access is set to 'team' for a specific scope.
|
||||
*/
|
||||
public function checkReadOnlyTeam(string $scope): bool
|
||||
{
|
||||
return $this->aclManager->checkReadOnlyTeam($this->user, $scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether 'read' access is set to 'own' for a specific scope.
|
||||
*/
|
||||
public function checkReadOnlyOwn(string $scope): bool
|
||||
{
|
||||
return $this->aclManager->checkReadOnlyOwn($this->user, $scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether 'read' access is set to 'all' for a specific scope.
|
||||
*/
|
||||
public function checkReadAll(string $scope): bool
|
||||
{
|
||||
return $this->aclManager->checkReadAll($this->user, $scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a scope or entity. If $action is omitted, it will check
|
||||
* whether a scope level is set to 'enabled'.
|
||||
*
|
||||
* @param string|Entity $subject An entity type or entity.
|
||||
* @param Table::ACTION_*|null $action Action to check. Constants are available in the `Table` class.
|
||||
* @throws NotImplemented
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function check($subject, ?string $action = null): bool
|
||||
{
|
||||
return $this->aclManager->check($this->user, $subject, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* The same as `check` but does not throw NotImplemented exception.
|
||||
*
|
||||
* @param string|Entity $subject An entity type or entity.
|
||||
* @param Table::ACTION_*|null $action Action to check. Constants are available in the `Table` class.
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function tryCheck($subject, ?string $action = null): bool
|
||||
{
|
||||
return $this->aclManager->tryCheck($this->user, $subject, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access to scope. If $action is omitted, it will check
|
||||
* whether a scope level is set to 'enabled'.
|
||||
*
|
||||
* @throws NotImplemented
|
||||
*/
|
||||
public function checkScope(string $scope, ?string $action = null): bool
|
||||
{
|
||||
return $this->aclManager->checkScope($this->user, $scope, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access to a specific entity.
|
||||
*
|
||||
* @param Entity $entity An entity to check.
|
||||
* @param Table::ACTION_* $action Action to check. Constants are available in the `Table` class.
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function checkEntity(Entity $entity, string $action = Table::ACTION_READ): bool
|
||||
{
|
||||
return $this->aclManager->checkEntity($this->user, $entity, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 'read' access to a specific entity.
|
||||
*/
|
||||
public function checkEntityRead(Entity $entity): bool
|
||||
{
|
||||
/** @noinspection PhpRedundantOptionalArgumentInspection */
|
||||
return $this->checkEntity($entity, Table::ACTION_READ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 'create' access to a specific entity.
|
||||
*/
|
||||
public function checkEntityCreate(Entity $entity): bool
|
||||
{
|
||||
return $this->checkEntity($entity, Table::ACTION_CREATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 'edit' access to a specific entity.
|
||||
*/
|
||||
public function checkEntityEdit(Entity $entity): bool
|
||||
{
|
||||
return $this->checkEntity($entity, Table::ACTION_EDIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 'delete' access to a specific entity.
|
||||
*/
|
||||
public function checkEntityDelete(Entity $entity): bool
|
||||
{
|
||||
return $this->checkEntity($entity, Table::ACTION_DELETE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 'stream' access to a specific entity.
|
||||
*/
|
||||
public function checkEntityStream(Entity $entity): bool
|
||||
{
|
||||
return $this->checkEntity($entity, Table::ACTION_STREAM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a user is an owner of an entity.
|
||||
*/
|
||||
public function checkOwnershipOwn(Entity $entity): bool
|
||||
{
|
||||
return $this->aclManager->checkOwnershipOwn($this->user, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an entity belongs to a user team.
|
||||
*/
|
||||
public function checkOwnershipTeam(Entity $entity): bool
|
||||
{
|
||||
return $this->aclManager->checkOwnershipTeam($this->user, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an entity is shared with a user.
|
||||
*
|
||||
* @param Table::ACTION_* $action
|
||||
* @since 9.0.0
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function checkOwnershipShared(Entity $entity, string $action): bool
|
||||
{
|
||||
return $this->aclManager->checkOwnershipShared($this->user, $entity, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attributes forbidden for a user.
|
||||
*
|
||||
* @param Table::ACTION_READ|Table::ACTION_EDIT $action An action.
|
||||
* @param string $thresholdLevel Should not be used. Stands for possible future enhancements.
|
||||
* @return string[]
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function getScopeForbiddenAttributeList(
|
||||
string $scope,
|
||||
string $action = Table::ACTION_READ,
|
||||
string $thresholdLevel = Table::LEVEL_NO
|
||||
): array {
|
||||
|
||||
return $this->aclManager
|
||||
->getScopeForbiddenAttributeList($this->user, $scope, $action, $thresholdLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fields forbidden for a user.
|
||||
*
|
||||
* @param Table::ACTION_READ|Table::ACTION_EDIT $action An action.
|
||||
* @param string $thresholdLevel Should not be used. Stands for possible future enhancements.
|
||||
* @return string[]
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function getScopeForbiddenFieldList(
|
||||
string $scope,
|
||||
string $action = Table::ACTION_READ,
|
||||
string $thresholdLevel = Table::LEVEL_NO
|
||||
): array {
|
||||
|
||||
return $this->aclManager
|
||||
->getScopeForbiddenFieldList($this->user, $scope, $action, $thresholdLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access to a field.
|
||||
*
|
||||
* @param string $scope A scope (entity type).
|
||||
* @param string $field A field to check.
|
||||
* @param Table::ACTION_READ|Table::ACTION_EDIT $action An action.
|
||||
* @return bool
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function checkField(string $scope, string $field, string $action = Table::ACTION_READ): bool
|
||||
{
|
||||
return $this->aclManager->checkField($this->user, $scope, $field, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get links forbidden for a user.
|
||||
*
|
||||
* @param Table::ACTION_READ|Table::ACTION_EDIT $action An action.
|
||||
* @param string $thresholdLevel Should not be used. Stands for possible future enhancements.
|
||||
* @return string[]
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function getScopeForbiddenLinkList(
|
||||
string $scope,
|
||||
string $action = Table::ACTION_READ,
|
||||
string $thresholdLevel = Table::LEVEL_NO
|
||||
): array {
|
||||
|
||||
return $this->aclManager->getScopeForbiddenLinkList($this->user, $scope, $action, $thresholdLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a user has access to another user over a specific permission.
|
||||
*
|
||||
* @param User|string $target User entity or user ID.
|
||||
*/
|
||||
public function checkUserPermission($target, string $permissionType = Permission::USER): bool
|
||||
{
|
||||
return $this->aclManager->checkUserPermission($this->user, $target, $permissionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a user can assign to another user.
|
||||
*
|
||||
* @param User|string $target User entity or user ID.
|
||||
*/
|
||||
public function checkAssignmentPermission($target): bool
|
||||
{
|
||||
return $this->aclManager->checkAssignmentPermission($this->user, $target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a restricted field list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedFieldList(string $scope, $type): array
|
||||
{
|
||||
return $this->aclManager->getScopeRestrictedFieldList($scope, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a restricted attribute list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedAttributeList(string $scope, $type): array
|
||||
{
|
||||
return $this->aclManager->getScopeRestrictedAttributeList($scope, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a restricted link list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedLinkList(string $scope, $type): array
|
||||
{
|
||||
return $this->aclManager->getScopeRestrictedLinkList($scope, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `getPermissionLevel` instead.
|
||||
*/
|
||||
public function get(string $permission): string
|
||||
{
|
||||
return $this->getPermissionLevel($permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `checkOwnershipOwn` instead.
|
||||
*/
|
||||
public function checkIsOwner(Entity $entity): bool
|
||||
{
|
||||
return $this->aclManager->checkOwnershipOwn($this->user, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `checkOwnershipTeam` instead.
|
||||
*/
|
||||
public function checkInTeam(Entity $entity): bool
|
||||
{
|
||||
return $this->aclManager->checkOwnershipTeam($this->user, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `checkUserPermission` instead.
|
||||
*/
|
||||
public function checkUser(string $permission, User $entity): bool
|
||||
{
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
return $this->aclManager->checkUser($this->user, $permission, $entity);
|
||||
}
|
||||
}
|
||||
45
application/Espo/Core/Acl/AccessChecker.php
Normal file
45
application/Espo/Core/Acl/AccessChecker.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\Acl;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
/**
|
||||
* Bindings:
|
||||
* - `$entityType` – as of v9.1.0.
|
||||
* - `Espo\Core\AclManager`
|
||||
*/
|
||||
interface AccessChecker
|
||||
{
|
||||
/**
|
||||
* Check access to a scope.
|
||||
*/
|
||||
public function check(User $user, ScopeData $data): bool;
|
||||
}
|
||||
103
application/Espo/Core/Acl/AccessChecker/AccessCheckerFactory.php
Normal file
103
application/Espo/Core/Acl/AccessChecker/AccessCheckerFactory.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?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\Acl\AccessChecker;
|
||||
|
||||
use Espo\Core\Acl\AccessChecker;
|
||||
use Espo\Core\Acl\DefaultAccessChecker;
|
||||
use Espo\Core\Acl\Exceptions\NotImplemented;
|
||||
use Espo\Core\AclManager;
|
||||
use Espo\Core\Binding\Binder;
|
||||
use Espo\Core\Binding\BindingContainer;
|
||||
use Espo\Core\Binding\BindingData;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
class AccessCheckerFactory
|
||||
{
|
||||
/** @var class-string<AccessChecker> */
|
||||
private string $defaultClassName = DefaultAccessChecker::class;
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create an access checker.
|
||||
*
|
||||
* @throws NotImplemented
|
||||
*/
|
||||
public function create(string $scope, AclManager $aclManager): AccessChecker
|
||||
{
|
||||
$className = $this->getClassName($scope);
|
||||
|
||||
$bindingContainer = $this->createBindingContainer($className, $aclManager, $scope);
|
||||
|
||||
return $this->injectableFactory->createWithBinding($className, $bindingContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<AccessChecker>
|
||||
* @throws NotImplemented
|
||||
*/
|
||||
private function getClassName(string $scope): string
|
||||
{
|
||||
/** @var ?class-string<AccessChecker> $className1 */
|
||||
$className1 = $this->metadata->get(['aclDefs', $scope, 'accessCheckerClassName']);
|
||||
|
||||
if ($className1) {
|
||||
return $className1;
|
||||
}
|
||||
|
||||
if (!$this->metadata->get(['scopes', $scope])) {
|
||||
throw new NotImplemented("Access checker is not implemented for '$scope'.");
|
||||
}
|
||||
|
||||
return $this->defaultClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<AccessChecker> $className
|
||||
*/
|
||||
private function createBindingContainer(string $className, AclManager $aclManager, string $scope): BindingContainer
|
||||
{
|
||||
$bindingData = new BindingData();
|
||||
|
||||
$binder = new Binder($bindingData);
|
||||
|
||||
$binder->bindInstance(AclManager::class, $aclManager);
|
||||
|
||||
$binder
|
||||
->for($className)
|
||||
->bindValue('$entityType', $scope);
|
||||
|
||||
return new BindingContainer($bindingData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?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\Acl\AccessChecker\AccessCheckers;
|
||||
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Acl\DefaultAccessChecker;
|
||||
use Espo\Core\Acl\Traits\DefaultAccessCheckerDependency;
|
||||
use Espo\Core\Acl\AccessEntityCreateChecker;
|
||||
use Espo\Core\Acl\AccessEntityReadChecker;
|
||||
use Espo\Core\Acl\AccessEntityEditChecker;
|
||||
use Espo\Core\Acl\AccessEntityDeleteChecker;
|
||||
use Espo\Core\Acl\AccessEntityStreamChecker;
|
||||
use Espo\Core\Acl\ScopeData;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Access is determined by access to a foreign entity.
|
||||
*
|
||||
* @implements AccessEntityCreateChecker<Entity>
|
||||
* @implements AccessEntityReadChecker<Entity>
|
||||
* @implements AccessEntityEditChecker<Entity>
|
||||
* @implements AccessEntityDeleteChecker<Entity>
|
||||
* @implements AccessEntityStreamChecker<Entity>
|
||||
*/
|
||||
class Foreign implements
|
||||
|
||||
AccessEntityCreateChecker,
|
||||
AccessEntityReadChecker,
|
||||
AccessEntityEditChecker,
|
||||
AccessEntityDeleteChecker,
|
||||
AccessEntityStreamChecker
|
||||
{
|
||||
use DefaultAccessCheckerDependency;
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
DefaultAccessChecker $defaultAccessChecker,
|
||||
private EntityManager $entityManager
|
||||
) {
|
||||
$this->defaultAccessChecker = $defaultAccessChecker;
|
||||
}
|
||||
|
||||
private function getForeignEntity(Entity $entity): ?Entity
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$link = $this->metadata->get(['aclDefs', $entityType, 'link']);
|
||||
|
||||
if (!$link) {
|
||||
throw new LogicException("No `link` in aclDefs for {$entityType}.");
|
||||
}
|
||||
|
||||
if ($entity->isNew()) {
|
||||
$foreignEntityType = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType)
|
||||
->getRelation($link)
|
||||
->getForeignEntityType();
|
||||
|
||||
/** @var ?string $id */
|
||||
$id = $entity->get($link . 'Id');
|
||||
|
||||
if (!$id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->entityManager->getEntityById($foreignEntityType, $id);
|
||||
}
|
||||
|
||||
return $this->entityManager
|
||||
->getRDBRepository($entityType)
|
||||
->getRelation($entity, $link)
|
||||
->findOne();
|
||||
}
|
||||
|
||||
public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// @todo Check parent 'edit' access.
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityCreate($user, $foreign, $data);
|
||||
}
|
||||
|
||||
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityRead($user, $foreign, $data);
|
||||
}
|
||||
|
||||
public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityEdit($user, $foreign, $data);
|
||||
}
|
||||
|
||||
public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityDelete($user, $foreign, $data);
|
||||
}
|
||||
|
||||
public function checkEntityStream(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityStream($user, $foreign, $data);
|
||||
}
|
||||
}
|
||||
91
application/Espo/Core/Acl/AccessChecker/ScopeChecker.php
Normal file
91
application/Espo/Core/Acl/AccessChecker/ScopeChecker.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?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\Acl\AccessChecker;
|
||||
|
||||
use Espo\Core\Acl\ScopeData;
|
||||
use Espo\Core\Acl\Table;
|
||||
|
||||
/**
|
||||
* Checks scope access.
|
||||
*/
|
||||
class ScopeChecker
|
||||
{
|
||||
public function __construct()
|
||||
{}
|
||||
|
||||
public function check(ScopeData $data, ?string $action = null, ?ScopeCheckerData $checkerData = null): bool
|
||||
{
|
||||
if ($data->isFalse()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($data->isTrue()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($action === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$level = $data->get($action);
|
||||
|
||||
if ($level === Table::LEVEL_ALL || $level === Table::LEVEL_YES) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($level === Table::LEVEL_NO) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$checkerData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($level === Table::LEVEL_OWN || $level === Table::LEVEL_TEAM) {
|
||||
if ($checkerData->isOwn()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($level === Table::LEVEL_OWN || $level === Table::LEVEL_TEAM) {
|
||||
if ($checkerData->isShared()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($level === Table::LEVEL_TEAM) {
|
||||
if ($checkerData->inTeam()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
64
application/Espo/Core/Acl/AccessChecker/ScopeCheckerData.php
Normal file
64
application/Espo/Core/Acl/AccessChecker/ScopeCheckerData.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?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\Acl\AccessChecker;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Scope checker data.
|
||||
*/
|
||||
class ScopeCheckerData
|
||||
{
|
||||
public function __construct(
|
||||
private Closure $isOwnChecker,
|
||||
private Closure $inTeamChecker,
|
||||
private Closure $isSharedChecker,
|
||||
) {}
|
||||
|
||||
public function isOwn(): bool
|
||||
{
|
||||
return ($this->isOwnChecker)();
|
||||
}
|
||||
|
||||
public function inTeam(): bool
|
||||
{
|
||||
return ($this->inTeamChecker)();
|
||||
}
|
||||
|
||||
public function isShared(): bool
|
||||
{
|
||||
return ($this->isSharedChecker)();
|
||||
}
|
||||
|
||||
public static function createBuilder(): ScopeCheckerDataBuilder
|
||||
{
|
||||
return new ScopeCheckerDataBuilder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?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\Acl\AccessChecker;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Builds scope checker data.
|
||||
*/
|
||||
class ScopeCheckerDataBuilder
|
||||
{
|
||||
private Closure $isOwnChecker;
|
||||
private Closure $inTeamChecker;
|
||||
private Closure $isSharedChecker;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->isOwnChecker = fn(): bool => false;
|
||||
$this->inTeamChecker = fn(): bool => false;
|
||||
$this->isSharedChecker = fn(): bool => false;
|
||||
}
|
||||
|
||||
public function setIsOwn(bool $value): self
|
||||
{
|
||||
if ($value) {
|
||||
$this->isOwnChecker = function (): bool {
|
||||
return true;
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->isOwnChecker = function (): bool {
|
||||
return false;
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setInTeam(bool $value): self
|
||||
{
|
||||
if ($value) {
|
||||
$this->inTeamChecker = function (): bool {
|
||||
return true;
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->inTeamChecker = function (): bool {
|
||||
return false;
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setIsShared(bool $value): self
|
||||
{
|
||||
if ($value) {
|
||||
$this->isSharedChecker = fn(): bool => true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->isSharedChecker = fn(): bool => false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure(): bool $checker
|
||||
*/
|
||||
public function setIsOwnChecker(Closure $checker): self
|
||||
{
|
||||
$this->isOwnChecker = $checker;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure(): bool $checker
|
||||
*/
|
||||
public function setInTeamChecker(Closure $checker): self
|
||||
{
|
||||
$this->inTeamChecker = $checker;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure(): bool $checker
|
||||
*/
|
||||
public function setIsSharedChecker(Closure $checker): self
|
||||
{
|
||||
$this->isSharedChecker = $checker;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function build(): ScopeCheckerData
|
||||
{
|
||||
return new ScopeCheckerData($this->isOwnChecker, $this->inTeamChecker, $this->isSharedChecker);
|
||||
}
|
||||
}
|
||||
40
application/Espo/Core/Acl/AccessCreateChecker.php
Normal file
40
application/Espo/Core/Acl/AccessCreateChecker.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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\Acl;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
interface AccessCreateChecker extends AccessChecker
|
||||
{
|
||||
/**
|
||||
* Check 'create' access.
|
||||
*/
|
||||
public function checkCreate(User $user, ScopeData $data): bool;
|
||||
}
|
||||
40
application/Espo/Core/Acl/AccessDeleteChecker.php
Normal file
40
application/Espo/Core/Acl/AccessDeleteChecker.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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\Acl;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
interface AccessDeleteChecker extends AccessChecker
|
||||
{
|
||||
/**
|
||||
* Check 'delete' access.
|
||||
*/
|
||||
public function checkDelete(User $user, ScopeData $data): bool;
|
||||
}
|
||||
40
application/Espo/Core/Acl/AccessEditChecker.php
Normal file
40
application/Espo/Core/Acl/AccessEditChecker.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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\Acl;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
interface AccessEditChecker extends AccessChecker
|
||||
{
|
||||
/**
|
||||
* Check 'edit' access.
|
||||
*/
|
||||
public function checkEdit(User $user, ScopeData $data): bool;
|
||||
}
|
||||
48
application/Espo/Core/Acl/AccessEntityCREDChecker.php
Normal file
48
application/Espo/Core/Acl/AccessEntityCREDChecker.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\Acl;
|
||||
|
||||
/**
|
||||
* @template TEntity of \Espo\ORM\Entity
|
||||
* @extends AccessEntityCreateChecker<TEntity>
|
||||
* @extends AccessEntityCreateChecker<TEntity>
|
||||
* @extends AccessEntityReadChecker<TEntity>
|
||||
* @extends AccessEntityEditChecker<TEntity>
|
||||
* @extends AccessEntityDeleteChecker<TEntity>
|
||||
*/
|
||||
interface AccessEntityCREDChecker extends
|
||||
|
||||
AccessEntityCreateChecker,
|
||||
AccessEntityReadChecker,
|
||||
AccessEntityEditChecker,
|
||||
AccessEntityDeleteChecker
|
||||
{
|
||||
|
||||
}
|
||||
47
application/Espo/Core/Acl/AccessEntityCREDSChecker.php
Normal file
47
application/Espo/Core/Acl/AccessEntityCREDSChecker.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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\Acl;
|
||||
|
||||
/**
|
||||
* @template TEntity of \Espo\ORM\Entity
|
||||
* @extends AccessEntityCreateChecker<TEntity>
|
||||
* @extends AccessEntityCreateChecker<TEntity>
|
||||
* @extends AccessEntityReadChecker<TEntity>
|
||||
* @extends AccessEntityEditChecker<TEntity>
|
||||
* @extends AccessEntityDeleteChecker<TEntity>
|
||||
* @extends AccessEntityStreamChecker<TEntity>
|
||||
*/
|
||||
interface AccessEntityCREDSChecker extends
|
||||
AccessEntityCreateChecker,
|
||||
AccessEntityReadChecker,
|
||||
AccessEntityEditChecker,
|
||||
AccessEntityDeleteChecker,
|
||||
AccessEntityStreamChecker
|
||||
{}
|
||||
46
application/Espo/Core/Acl/AccessEntityCreateChecker.php
Normal file
46
application/Espo/Core/Acl/AccessEntityCreateChecker.php
Normal file
@@ -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\Acl;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface AccessEntityCreateChecker extends AccessCreateChecker
|
||||
{
|
||||
/**
|
||||
* Check 'create' access for an entity.
|
||||
*
|
||||
* @param TEntity $entity
|
||||
*/
|
||||
public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool;
|
||||
}
|
||||
46
application/Espo/Core/Acl/AccessEntityDeleteChecker.php
Normal file
46
application/Espo/Core/Acl/AccessEntityDeleteChecker.php
Normal file
@@ -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\Acl;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface AccessEntityDeleteChecker extends AccessDeleteChecker
|
||||
{
|
||||
/**
|
||||
* Check 'delete' access for an entity.
|
||||
*
|
||||
* @param TEntity $entity
|
||||
*/
|
||||
public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool;
|
||||
}
|
||||
46
application/Espo/Core/Acl/AccessEntityEditChecker.php
Normal file
46
application/Espo/Core/Acl/AccessEntityEditChecker.php
Normal file
@@ -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\Acl;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface AccessEntityEditChecker extends AccessEditChecker
|
||||
{
|
||||
/**
|
||||
* Check 'edit' access for an entity.
|
||||
*
|
||||
* @param TEntity $entity
|
||||
*/
|
||||
public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool;
|
||||
}
|
||||
46
application/Espo/Core/Acl/AccessEntityReadChecker.php
Normal file
46
application/Espo/Core/Acl/AccessEntityReadChecker.php
Normal file
@@ -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\Acl;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface AccessEntityReadChecker extends AccessReadChecker
|
||||
{
|
||||
/**
|
||||
* Check 'read' access for entity.
|
||||
*
|
||||
* @param TEntity $entity
|
||||
*/
|
||||
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool;
|
||||
}
|
||||
46
application/Espo/Core/Acl/AccessEntityStreamChecker.php
Normal file
46
application/Espo/Core/Acl/AccessEntityStreamChecker.php
Normal file
@@ -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\Acl;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface AccessEntityStreamChecker extends AccessStreamChecker
|
||||
{
|
||||
/**
|
||||
* Check 'stream' access for an entity.
|
||||
*
|
||||
* @param TEntity $entity
|
||||
*/
|
||||
public function checkEntityStream(User $user, Entity $entity, ScopeData $data): bool;
|
||||
}
|
||||
40
application/Espo/Core/Acl/AccessReadChecker.php
Normal file
40
application/Espo/Core/Acl/AccessReadChecker.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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\Acl;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
interface AccessReadChecker extends AccessChecker
|
||||
{
|
||||
/**
|
||||
* Check 'read' access.
|
||||
*/
|
||||
public function checkRead(User $user, ScopeData $data): bool;
|
||||
}
|
||||
40
application/Espo/Core/Acl/AccessStreamChecker.php
Normal file
40
application/Espo/Core/Acl/AccessStreamChecker.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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\Acl;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
interface AccessStreamChecker extends AccessChecker
|
||||
{
|
||||
/**
|
||||
* Check 'stream' access.
|
||||
*/
|
||||
public function checkStream(User $user, ScopeData $data): bool;
|
||||
}
|
||||
47
application/Espo/Core/Acl/AssignmentChecker.php
Normal file
47
application/Espo/Core/Acl/AssignmentChecker.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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\Acl;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface AssignmentChecker
|
||||
{
|
||||
/**
|
||||
* Check assignment.
|
||||
*
|
||||
* @param TEntity $entity
|
||||
*/
|
||||
public function check(User $user, Entity $entity): bool;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?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\Acl\AssignmentChecker;
|
||||
|
||||
use Espo\Core\Acl\AssignmentChecker;
|
||||
use Espo\Core\Acl\DefaultAssignmentChecker;
|
||||
use Espo\Core\Acl\Exceptions\NotImplemented;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class AssignmentCheckerFactory
|
||||
{
|
||||
/** @var class-string<AssignmentChecker<CoreEntity>> */
|
||||
private string $defaultClassName = DefaultAssignmentChecker::class;
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create an access checker.
|
||||
*
|
||||
* @return AssignmentChecker<Entity>
|
||||
* @throws NotImplemented
|
||||
*/
|
||||
public function create(string $scope): AssignmentChecker
|
||||
{
|
||||
$className = $this->getClassName($scope);
|
||||
|
||||
return $this->injectableFactory->create($className);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<AssignmentChecker<Entity>>
|
||||
* @throws NotImplemented
|
||||
*/
|
||||
private function getClassName(string $scope): string
|
||||
{
|
||||
/** @var ?class-string<AssignmentChecker<Entity>> $className */
|
||||
$className = $this->metadata->get(['aclDefs', $scope, 'assignmentCheckerClassName']);
|
||||
|
||||
if ($className) {
|
||||
return $className;
|
||||
}
|
||||
|
||||
if (!$this->metadata->get(['scopes', $scope])) {
|
||||
throw new NotImplemented();
|
||||
}
|
||||
|
||||
/** @var class-string<AssignmentChecker<Entity>> */
|
||||
return $this->defaultClassName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?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\Acl\AssignmentChecker;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Core\Acl\AssignmentChecker;
|
||||
|
||||
class AssignmentCheckerManager
|
||||
{
|
||||
/** @var array<string, AssignmentChecker<Entity>> */
|
||||
private $checkerCache = [];
|
||||
|
||||
public function __construct(private AssignmentCheckerFactory $factory)
|
||||
{}
|
||||
|
||||
public function check(User $user, Entity $entity): bool
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$checker = $this->getChecker($entityType);
|
||||
|
||||
return $checker->check($user, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AssignmentChecker<Entity>
|
||||
*/
|
||||
private function getChecker(string $entityType): AssignmentChecker
|
||||
{
|
||||
if (!array_key_exists($entityType, $this->checkerCache)) {
|
||||
$this->loadChecker($entityType);
|
||||
}
|
||||
|
||||
return $this->checkerCache[$entityType];
|
||||
}
|
||||
|
||||
private function loadChecker(string $entityType): void
|
||||
{
|
||||
$this->checkerCache[$entityType] = $this->factory->create($entityType);
|
||||
}
|
||||
}
|
||||
339
application/Espo/Core/Acl/AssignmentChecker/Helper.php
Normal file
339
application/Espo/Core/Acl/AssignmentChecker/Helper.php
Normal file
@@ -0,0 +1,339 @@
|
||||
<?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\Acl\AssignmentChecker;
|
||||
|
||||
use Espo\Core\Acl\Permission;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\AclManager;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
use Espo\Repositories\User as UserRepository;
|
||||
|
||||
class Helper
|
||||
{
|
||||
private const ATTR_ASSIGNED_USER_ID = Field::ASSIGNED_USER . 'Id';
|
||||
private const FIELD_TEAMS = Field::TEAMS;
|
||||
private const FIELD_ASSIGNED_USERS = Field::ASSIGNED_USERS;
|
||||
private const FIELD_COLLABORATORS = Field::COLLABORATORS;
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private AclManager $aclManager,
|
||||
private Defs $ormDefs,
|
||||
private Metadata $metadata,
|
||||
) {}
|
||||
|
||||
public function checkAssignedUser(User $user, Entity $entity): bool
|
||||
{
|
||||
if (!$entity->hasAttribute(self::ATTR_ASSIGNED_USER_ID)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($user->isPortal()) {
|
||||
return !$entity->isAttributeChanged(self::ATTR_ASSIGNED_USER_ID);
|
||||
}
|
||||
|
||||
$assignmentPermission = $this->aclManager->getPermissionLevel($user, Permission::ASSIGNMENT);
|
||||
|
||||
if ($assignmentPermission === Table::LEVEL_ALL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$toProcess = false;
|
||||
|
||||
if (!$entity->isNew()) {
|
||||
if ($entity->isAttributeChanged(self::ATTR_ASSIGNED_USER_ID)) {
|
||||
$toProcess = true;
|
||||
}
|
||||
} else {
|
||||
$toProcess = true;
|
||||
}
|
||||
|
||||
if (!$toProcess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$assignedUserId = $entity->get(self::ATTR_ASSIGNED_USER_ID);
|
||||
|
||||
if (!$assignedUserId) {
|
||||
if ($assignmentPermission === Table::LEVEL_NO && !$user->isApi()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($assignmentPermission === Table::LEVEL_NO) {
|
||||
return $user->getId() === $assignedUserId;
|
||||
}
|
||||
|
||||
if ($assignmentPermission === Table::LEVEL_TEAM) {
|
||||
$teamIdList = $user->getTeamIdList();
|
||||
|
||||
return $this->getUserRepository()->checkBelongsToAnyOfTeams($assignedUserId, $teamIdList);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function checkTeams(User $user, Entity $entity): bool
|
||||
{
|
||||
$assignmentPermission = $this->aclManager->getPermissionLevel($user, Permission::ASSIGNMENT);
|
||||
|
||||
if (!in_array($assignmentPermission, [Table::LEVEL_TEAM, Table::LEVEL_NO])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$entity->hasLinkMultipleField(self::FIELD_TEAMS)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$teamIdList = $entity->getLinkMultipleIdList(self::FIELD_TEAMS);
|
||||
|
||||
if ($teamIdList === []) {
|
||||
return $this->isPermittedTeamsEmpty($user, $entity);
|
||||
}
|
||||
|
||||
$newIdList = [];
|
||||
|
||||
if (!$entity->isNew()) {
|
||||
$existingIdList = [];
|
||||
|
||||
$teamCollection = $this->entityManager
|
||||
->getRelation($entity, self::FIELD_TEAMS)
|
||||
->select(Attribute::ID)
|
||||
->find();
|
||||
|
||||
foreach ($teamCollection as $team) {
|
||||
$existingIdList[] = $team->getId();
|
||||
}
|
||||
|
||||
foreach ($teamIdList as $id) {
|
||||
if (!in_array($id, $existingIdList)) {
|
||||
$newIdList[] = $id;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$newIdList = $teamIdList;
|
||||
}
|
||||
|
||||
if ($newIdList === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$userTeamIdList = $user->getLinkMultipleIdList(self::FIELD_TEAMS);
|
||||
|
||||
foreach ($newIdList as $id) {
|
||||
if (!in_array($id, $userTeamIdList)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkUsers(User $user, Entity $entity, string $field): bool
|
||||
{
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$idsAttr = $field . 'Ids';
|
||||
|
||||
if (!$entity->hasLinkMultipleField($field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($user->isPortal()) {
|
||||
if (!$entity->isAttributeChanged($idsAttr)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$assignmentPermission = $this->aclManager->getPermissionLevel($user, Permission::ASSIGNMENT);
|
||||
|
||||
if ($assignmentPermission === Table::LEVEL_ALL) {
|
||||
if (!$this->hasOnlyInternalUsers($user, $entity, $field)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$toProcess = $entity->isNew() || $entity->isAttributeChanged($idsAttr);
|
||||
|
||||
if (!$toProcess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$userIds = $entity->getLinkMultipleIdList($field);
|
||||
|
||||
if ($userIds === []) {
|
||||
if ($assignmentPermission === Table::LEVEL_NO && !$user->isApi()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($assignmentPermission === Table::LEVEL_NO) {
|
||||
return $this->isPermittedUsersLevelNo($user, $entity, $field);
|
||||
}
|
||||
|
||||
if ($assignmentPermission === Table::LEVEL_TEAM) {
|
||||
return $this->isPermittedUsersLevelTeam($user, $entity, $field);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isPermittedUsersLevelNo(User $user, CoreEntity $entity, string $field): bool
|
||||
{
|
||||
$userIds = $this->getAddedLinkMultipleIds($entity, $field);
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
if ($user->getId() !== $userId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function isPermittedUsersLevelTeam(User $user, CoreEntity $entity, string $field): bool
|
||||
{
|
||||
$teamIds = $user->getLinkMultipleIdList(self::FIELD_TEAMS);
|
||||
$userIds = $this->getAddedLinkMultipleIds($entity, $field);
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
if (!$this->getUserRepository()->checkBelongsToAnyOfTeams($userId, $teamIds)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function hasOnlyInternalUsers(User $user, CoreEntity $entity, string $field): bool
|
||||
{
|
||||
$ids = array_diff($this->getAddedLinkMultipleIds($entity, $field), [$user->getId()]);
|
||||
$ids = array_values($ids);
|
||||
|
||||
$count = $this->entityManager
|
||||
->getRDBRepositoryByClass(User::class)
|
||||
->where([
|
||||
'type!=' => [
|
||||
User::TYPE_REGULAR,
|
||||
User::TYPE_ADMIN,
|
||||
],
|
||||
Attribute::ID => $ids,
|
||||
])
|
||||
->count();
|
||||
|
||||
return $count === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getAddedLinkMultipleIds(CoreEntity $entity, string $field): array
|
||||
{
|
||||
/** @var string[] $previousIds */
|
||||
$previousIds = $entity->getFetched(self::FIELD_COLLABORATORS . 'Ids') ?? [];
|
||||
|
||||
return array_values(array_diff($entity->getLinkMultipleIdList($field), $previousIds));
|
||||
}
|
||||
|
||||
private function isPermittedTeamsEmpty(User $user, CoreEntity $entity): bool
|
||||
{
|
||||
$assignmentPermission = $this->aclManager->getPermissionLevel($user, Permission::ASSIGNMENT);
|
||||
|
||||
if ($assignmentPermission !== Table::LEVEL_TEAM) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($entity->hasLinkMultipleField(self::FIELD_ASSIGNED_USERS)) {
|
||||
$assignedUserIdList = $entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS);
|
||||
|
||||
if ($assignedUserIdList === []) {
|
||||
return false;
|
||||
}
|
||||
} else if ($entity->hasAttribute(self::ATTR_ASSIGNED_USER_ID)) {
|
||||
if (!$entity->get(self::ATTR_ASSIGNED_USER_ID)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function hasAssignedUsersField(string $entityType): bool
|
||||
{
|
||||
$entityDefs = $this->ormDefs->getEntity($entityType);
|
||||
|
||||
return
|
||||
$entityDefs->hasField(self::FIELD_ASSIGNED_USERS) &&
|
||||
$entityDefs->getField(self::FIELD_ASSIGNED_USERS)->getType() === FieldType::LINK_MULTIPLE &&
|
||||
$entityDefs->hasRelation(self::FIELD_ASSIGNED_USERS) &&
|
||||
$entityDefs->getRelation(self::FIELD_ASSIGNED_USERS)->getForeignEntityType() === User::ENTITY_TYPE;
|
||||
}
|
||||
|
||||
public function hasCollaboratorsField(string $entityType): bool
|
||||
{
|
||||
if (!$this->metadata->get("scopes.$entityType.collaborators")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entityDefs = $this->ormDefs->getEntity($entityType);
|
||||
|
||||
return
|
||||
$entityDefs->tryGetField(self::FIELD_COLLABORATORS)?->getType() === FieldType::LINK_MULTIPLE &&
|
||||
$entityDefs->tryGetRelation(self::FIELD_COLLABORATORS)?->tryGetForeignEntityType() === User::ENTITY_TYPE;
|
||||
}
|
||||
|
||||
private function getUserRepository(): UserRepository
|
||||
{
|
||||
/** @var UserRepository */
|
||||
return $this->entityManager->getRepository(User::ENTITY_TYPE);
|
||||
}
|
||||
}
|
||||
86
application/Espo/Core/Acl/Cache/Clearer.php
Normal file
86
application/Espo/Core/Acl/Cache/Clearer.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?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\Acl\Cache;
|
||||
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
use Espo\Entities\Portal;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
|
||||
/**
|
||||
* @todo Clear cache in AclManager.
|
||||
*/
|
||||
class Clearer
|
||||
{
|
||||
public function __construct(private FileManager $fileManager, private EntityManager $entityManager)
|
||||
{}
|
||||
|
||||
public function clearForAllInternalUsers(): void
|
||||
{
|
||||
$this->fileManager->removeInDir('data/cache/application/acl');
|
||||
$this->fileManager->removeInDir('data/cache/application/aclMap');
|
||||
}
|
||||
|
||||
public function clearForAllPortalUsers(): void
|
||||
{
|
||||
$this->fileManager->removeInDir('data/cache/application/aclPortal');
|
||||
$this->fileManager->removeInDir('data/cache/application/aclPortalMap');
|
||||
}
|
||||
|
||||
public function clearForUser(User $user): void
|
||||
{
|
||||
if ($user->isPortal()) {
|
||||
$this->clearForPortalUser($user);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$part = $user->getId() . '.php';
|
||||
|
||||
$this->fileManager->remove('data/cache/application/acl/' . $part);
|
||||
$this->fileManager->remove('data/cache/application/aclMap/' . $part);
|
||||
}
|
||||
|
||||
private function clearForPortalUser(User $user): void
|
||||
{
|
||||
$portals = $this->entityManager
|
||||
->getRDBRepositoryByClass(Portal::class)
|
||||
->select(Attribute::ID)
|
||||
->find();
|
||||
|
||||
foreach ($portals as $portal) {
|
||||
$part = $portal->getId() . '/' . $user->getId() . '.php';
|
||||
|
||||
$this->fileManager->remove('data/cache/application/aclPortal/' . $part);
|
||||
$this->fileManager->remove('data/cache/application/aclPortalMap/' . $part);
|
||||
}
|
||||
}
|
||||
}
|
||||
236
application/Espo/Core/Acl/DefaultAccessChecker.php
Normal file
236
application/Espo/Core/Acl/DefaultAccessChecker.php
Normal file
@@ -0,0 +1,236 @@
|
||||
<?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\Acl;
|
||||
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Acl\AccessChecker\ScopeChecker;
|
||||
use Espo\Core\Acl\AccessChecker\ScopeCheckerData;
|
||||
use Espo\Core\AclManager;
|
||||
use Espo\Core\Utils\Config;
|
||||
|
||||
use DateTime;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* A default implementation for access checking.
|
||||
*
|
||||
* @implements AccessEntityCreateChecker<Entity>
|
||||
* @implements AccessEntityReadChecker<Entity>
|
||||
* @implements AccessEntityEditChecker<Entity>
|
||||
* @implements AccessEntityDeleteChecker<Entity>
|
||||
* @implements AccessEntityStreamChecker<Entity>
|
||||
*/
|
||||
class DefaultAccessChecker implements
|
||||
|
||||
AccessEntityCreateChecker,
|
||||
AccessEntityReadChecker,
|
||||
AccessEntityEditChecker,
|
||||
AccessEntityDeleteChecker,
|
||||
AccessEntityStreamChecker
|
||||
{
|
||||
private const ATTR_CREATED_BY_ID = Field::CREATED_BY . 'Id';
|
||||
private const ATTR_CREATED_AT = Field::CREATED_AT;
|
||||
private const ATTR_ASSIGNED_USER_ID = Field::ASSIGNED_USER . 'Id';
|
||||
private const ALLOW_DELETE_OWN_CREATED_PERIOD = '24 hours';
|
||||
|
||||
public function __construct(
|
||||
private AclManager $aclManager,
|
||||
private Config $config,
|
||||
private ScopeChecker $scopeChecker
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param Table::ACTION_* $action
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
private function checkEntity(User $user, Entity $entity, ScopeData $data, string $action): bool
|
||||
{
|
||||
$checkerData = ScopeCheckerData
|
||||
::createBuilder()
|
||||
->setIsOwnChecker(
|
||||
fn(): bool => $this->aclManager->checkOwnershipOwn($user, $entity)
|
||||
)
|
||||
->setInTeamChecker(
|
||||
fn(): bool => $this->aclManager->checkOwnershipTeam($user, $entity)
|
||||
)
|
||||
->setIsSharedChecker(
|
||||
fn(): bool => $this->aclManager->checkOwnershipShared($user, $entity, $action)
|
||||
)
|
||||
->build();
|
||||
|
||||
return $this->scopeChecker->check($data, $action, $checkerData);
|
||||
}
|
||||
|
||||
private function checkScope(ScopeData $data, ?string $action = null): bool
|
||||
{
|
||||
$checkerData = ScopeCheckerData
|
||||
::createBuilder()
|
||||
->setIsOwn(true)
|
||||
->setInTeam(true)
|
||||
->setIsShared(true)
|
||||
->build();
|
||||
|
||||
return $this->scopeChecker->check($data, $action, $checkerData);
|
||||
}
|
||||
|
||||
public function check(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->checkScope($data);
|
||||
}
|
||||
|
||||
public function checkCreate(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->checkScope($data, Table::ACTION_CREATE);
|
||||
}
|
||||
|
||||
public function checkRead(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->checkScope($data, Table::ACTION_READ);
|
||||
}
|
||||
|
||||
public function checkEdit(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->checkScope($data, Table::ACTION_EDIT);
|
||||
}
|
||||
|
||||
public function checkDelete(User $user, ScopeData $data): bool
|
||||
{
|
||||
if ($this->checkScope($data, Table::ACTION_DELETE)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($data->getCreate() === Table::LEVEL_NO) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->config->get('aclAllowDeleteCreated')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function checkStream(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->checkScope($data, Table::ACTION_STREAM);
|
||||
}
|
||||
|
||||
public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
return $this->checkEntity($user, $entity, $data, Table::ACTION_CREATE);
|
||||
}
|
||||
|
||||
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
return $this->checkEntity($user, $entity, $data, Table::ACTION_READ);
|
||||
}
|
||||
|
||||
public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
return $this->checkEntity($user, $entity, $data, Table::ACTION_EDIT);
|
||||
}
|
||||
|
||||
public function checkEntityStream(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
return $this->checkEntity($user, $entity, $data, Table::ACTION_STREAM);
|
||||
}
|
||||
|
||||
public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
if ($this->checkEntity($user, $entity, $data, Table::ACTION_DELETE)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($data->getCreate() === Table::LEVEL_NO) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!$this->config->get('aclAllowDeleteCreated') ||
|
||||
!$entity->has(self::ATTR_CREATED_BY_ID) ||
|
||||
$entity->get(self::ATTR_CREATED_BY_ID) !== $user->getId()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$isDeletedAllowed = false;
|
||||
|
||||
if (!$entity->has(self::ATTR_ASSIGNED_USER_ID)) {
|
||||
$isDeletedAllowed = true;
|
||||
} else {
|
||||
if (!$entity->get(self::ATTR_ASSIGNED_USER_ID)) {
|
||||
$isDeletedAllowed = true;
|
||||
} else if ($entity->get(self::ATTR_ASSIGNED_USER_ID) === $entity->get(self::ATTR_CREATED_BY_ID)) {
|
||||
$isDeletedAllowed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isDeletedAllowed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$createdAt = $entity->get(self::ATTR_CREATED_AT);
|
||||
|
||||
if (!$createdAt) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$deleteThresholdPeriod =
|
||||
$this->config->get('aclAllowDeleteCreatedThresholdPeriod') ??
|
||||
self::ALLOW_DELETE_OWN_CREATED_PERIOD;
|
||||
|
||||
if (self::isDateTimeAfterPeriod($createdAt, $deleteThresholdPeriod)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function isDateTimeAfterPeriod(string $value, string $period): bool
|
||||
{
|
||||
try {
|
||||
$dt = new DateTime($value);
|
||||
} catch (Exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dt->modify($period);
|
||||
|
||||
$dtNow = new DateTime();
|
||||
|
||||
if ($dtNow->format('U') > $dt->format('U')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
92
application/Espo/Core/Acl/DefaultAssignmentChecker.php
Normal file
92
application/Espo/Core/Acl/DefaultAssignmentChecker.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?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\Acl;
|
||||
|
||||
use Espo\Core\Acl\AssignmentChecker\Helper;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
|
||||
/**
|
||||
* @implements AssignmentChecker<CoreEntity>
|
||||
*/
|
||||
class DefaultAssignmentChecker implements AssignmentChecker
|
||||
{
|
||||
protected const FIELD_ASSIGNED_USERS = Field::ASSIGNED_USERS;
|
||||
private const FIELD_COLLABORATORS = Field::COLLABORATORS;
|
||||
|
||||
public function __construct(
|
||||
private Helper $helper,
|
||||
) {}
|
||||
|
||||
public function check(User $user, Entity $entity): bool
|
||||
{
|
||||
if (!$this->isPermittedAssignedUser($user, $entity)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isPermittedTeams($user, $entity)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->helper->hasAssignedUsersField($entity->getEntityType())) {
|
||||
if (!$this->isPermittedAssignedUsers($user, $entity)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->helper->hasCollaboratorsField($entity->getEntityType())) {
|
||||
if (!$this->helper->checkUsers($user, $entity, self::FIELD_COLLABORATORS)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function isPermittedAssignedUser(User $user, Entity $entity): bool
|
||||
{
|
||||
return $this->helper->checkAssignedUser($user, $entity);
|
||||
}
|
||||
|
||||
protected function isPermittedTeams(User $user, Entity $entity): bool
|
||||
{
|
||||
return $this->helper->checkTeams($user, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Left for backward compatibility.
|
||||
*/
|
||||
protected function isPermittedAssignedUsers(User $user, Entity $entity): bool
|
||||
{
|
||||
return $this->helper->checkUsers($user, $entity, self::FIELD_ASSIGNED_USERS);
|
||||
}
|
||||
}
|
||||
135
application/Espo/Core/Acl/DefaultOwnershipChecker.php
Normal file
135
application/Espo/Core/Acl/DefaultOwnershipChecker.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?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\Acl;
|
||||
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Entities\User;
|
||||
|
||||
/**
|
||||
* A default implementation for ownership checking.
|
||||
*
|
||||
* @implements OwnershipOwnChecker<CoreEntity>
|
||||
* @implements OwnershipTeamChecker<CoreEntity>
|
||||
* @implements OwnershipSharedChecker<CoreEntity>
|
||||
*/
|
||||
class DefaultOwnershipChecker implements OwnershipOwnChecker, OwnershipTeamChecker, OwnershipSharedChecker
|
||||
{
|
||||
private const ATTR_CREATED_BY_ID = Field::CREATED_BY . 'Id';
|
||||
private const ATTR_ASSIGNED_USER_ID = Field::ASSIGNED_USER . 'Id';
|
||||
private const ATTR_ASSIGNED_TEAMS_IDS = Field::TEAMS . 'Ids';
|
||||
private const FIELD_TEAMS = Field::TEAMS;
|
||||
private const FIELD_ASSIGNED_USERS = Field::ASSIGNED_USERS;
|
||||
private const FIELD_COLLABORATORS = Field::COLLABORATORS;
|
||||
|
||||
public function checkOwn(User $user, Entity $entity): bool
|
||||
{
|
||||
if ($entity instanceof CoreEntity && $entity->hasLinkMultipleField(self::FIELD_ASSIGNED_USERS)) {
|
||||
if ($entity->hasLinkMultipleId(self::FIELD_ASSIGNED_USERS, $user->getId())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($entity->hasAttribute(self::ATTR_ASSIGNED_USER_ID)) {
|
||||
if (
|
||||
$entity->has(self::ATTR_ASSIGNED_USER_ID) &&
|
||||
$user->getId() === $entity->get(self::ATTR_ASSIGNED_USER_ID)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($entity->hasAttribute(self::ATTR_CREATED_BY_ID)) {
|
||||
if (
|
||||
$entity->has(self::ATTR_CREATED_BY_ID) &&
|
||||
$user->getId() === $entity->get(self::ATTR_CREATED_BY_ID)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function checkTeam(User $user, Entity $entity): bool
|
||||
{
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$userTeamIdList = $user->getLinkMultipleIdList(self::FIELD_TEAMS);
|
||||
|
||||
if (
|
||||
!$entity->hasRelation(self::FIELD_TEAMS) ||
|
||||
!$entity->hasAttribute(self::ATTR_ASSIGNED_TEAMS_IDS)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entityTeamIdList = $entity->getLinkMultipleIdList(self::FIELD_TEAMS);
|
||||
|
||||
if (empty($entityTeamIdList)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($userTeamIdList as $id) {
|
||||
if (in_array($id, $entityTeamIdList)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function checkShared(User $user, Entity $entity, string $action): bool
|
||||
{
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($action !== Table::ACTION_READ && $action !== Table::ACTION_STREAM) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!$entity->hasRelation(self::FIELD_COLLABORATORS) ||
|
||||
!$entity->hasLinkMultipleField(self::FIELD_COLLABORATORS)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($user->getId(), $entity->getLinkMultipleIdList(self::FIELD_COLLABORATORS));
|
||||
}
|
||||
}
|
||||
34
application/Espo/Core/Acl/Exceptions/NotAvailable.php
Normal file
34
application/Espo/Core/Acl/Exceptions/NotAvailable.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Acl\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class NotAvailable extends RuntimeException {}
|
||||
34
application/Espo/Core/Acl/Exceptions/NotImplemented.php
Normal file
34
application/Espo/Core/Acl/Exceptions/NotImplemented.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Acl\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class NotImplemented extends RuntimeException {}
|
||||
96
application/Espo/Core/Acl/FieldData.php
Normal file
96
application/Espo/Core/Acl/FieldData.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?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\Acl;
|
||||
|
||||
use stdClass;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Field data.
|
||||
*/
|
||||
class FieldData
|
||||
{
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $actionData = [];
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
/**
|
||||
* @return never
|
||||
*/
|
||||
public function __get(string $name)
|
||||
{
|
||||
throw new RuntimeException("Accessing ScopeData properties is not allowed.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a level for an action.
|
||||
*/
|
||||
public function get(string $action): string
|
||||
{
|
||||
return $this->actionData[$action] ?? Table::LEVEL_NO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a 'read' level.
|
||||
*/
|
||||
public function getRead(): string
|
||||
{
|
||||
return $this->get(Table::ACTION_READ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an 'edit' level.
|
||||
*/
|
||||
public function getEdit(): string
|
||||
{
|
||||
return $this->get(Table::ACTION_EDIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a raw table value.
|
||||
*/
|
||||
public static function fromRaw(stdClass $raw): self
|
||||
{
|
||||
$obj = new self();
|
||||
|
||||
$obj->actionData = get_object_vars($raw);
|
||||
|
||||
foreach ($obj->actionData as $item) {
|
||||
if (!is_string($item)) {
|
||||
throw new RuntimeException("Bad raw scope data.");
|
||||
}
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
273
application/Espo/Core/Acl/GlobalRestriction.php
Normal file
273
application/Espo/Core/Acl/GlobalRestriction.php
Normal file
@@ -0,0 +1,273 @@
|
||||
<?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\Acl;
|
||||
|
||||
use Espo\Core\Utils\Config\SystemConfig;
|
||||
use Espo\Core\Utils\DataCache;
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Lists of restricted fields can be obtained from here. Restricted fields
|
||||
* are specified in metadata > entityAcl.
|
||||
*/
|
||||
class GlobalRestriction
|
||||
{
|
||||
/** Totally forbidden. */
|
||||
public const TYPE_FORBIDDEN = 'forbidden';
|
||||
/** Reading forbidden, writing allowed. */
|
||||
public const TYPE_INTERNAL = 'internal';
|
||||
/** Forbidden for non-admin users. */
|
||||
public const TYPE_ONLY_ADMIN = 'onlyAdmin';
|
||||
/** Read-only for all users. */
|
||||
public const TYPE_READ_ONLY = 'readOnly';
|
||||
/** Read-only for non-admin users. */
|
||||
public const TYPE_NON_ADMIN_READ_ONLY = 'nonAdminReadOnly';
|
||||
|
||||
/**
|
||||
* @var array<int, self::TYPE_*>
|
||||
*/
|
||||
private $fieldTypeList = [
|
||||
self::TYPE_FORBIDDEN,
|
||||
self::TYPE_INTERNAL,
|
||||
self::TYPE_ONLY_ADMIN,
|
||||
self::TYPE_READ_ONLY,
|
||||
self::TYPE_NON_ADMIN_READ_ONLY,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<int, self::TYPE_*>
|
||||
*/
|
||||
private $linkTypeList = [
|
||||
self::TYPE_FORBIDDEN,
|
||||
self::TYPE_INTERNAL,
|
||||
self::TYPE_ONLY_ADMIN,
|
||||
self::TYPE_READ_ONLY,
|
||||
self::TYPE_NON_ADMIN_READ_ONLY,
|
||||
];
|
||||
|
||||
/**
|
||||
* Types that should also be taken from entityDefs.
|
||||
* @var array<int, self::TYPE_*>
|
||||
*/
|
||||
private array $entityDefsTypeList = [
|
||||
self::TYPE_READ_ONLY,
|
||||
];
|
||||
|
||||
private ?stdClass $data = null;
|
||||
|
||||
private string $cacheKey = 'entityAcl';
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private DataCache $dataCache,
|
||||
private FieldUtil $fieldUtil,
|
||||
SystemConfig $systemConfig,
|
||||
) {
|
||||
|
||||
$useCache = $systemConfig->useCache();
|
||||
|
||||
if ($useCache && $this->dataCache->has($this->cacheKey)) {
|
||||
/** @var stdClass $cachedData */
|
||||
$cachedData = $this->dataCache->get($this->cacheKey);
|
||||
|
||||
$this->data = $cachedData;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->data) {
|
||||
$this->buildData();
|
||||
}
|
||||
|
||||
if ($useCache) {
|
||||
$this->storeCacheFile();
|
||||
}
|
||||
}
|
||||
|
||||
private function storeCacheFile(): void
|
||||
{
|
||||
assert($this->data !== null);
|
||||
|
||||
$this->dataCache->store($this->cacheKey, $this->data);
|
||||
}
|
||||
|
||||
private function buildData(): void
|
||||
{
|
||||
/** @var string[] $scopeList */
|
||||
$scopeList = array_keys($this->metadata->get(['entityDefs']) ?? []);
|
||||
|
||||
$data = (object) [];
|
||||
|
||||
foreach ($scopeList as $scope) {
|
||||
/** @var string[] $fieldList */
|
||||
$fieldList = array_keys($this->metadata->get(['entityDefs', $scope, 'fields']) ?? []);
|
||||
/** @var string[] $linkList */
|
||||
$linkList = array_keys($this->metadata->get(['entityDefs', $scope, 'links']) ?? []);
|
||||
|
||||
$isNotEmpty = false;
|
||||
|
||||
$scopeData = (object) [
|
||||
'fields' => (object) [],
|
||||
'attributes' => (object) [],
|
||||
'links' => (object) [],
|
||||
];
|
||||
|
||||
foreach ($this->fieldTypeList as $type) {
|
||||
$resultFieldList = [];
|
||||
$resultAttributeList = [];
|
||||
|
||||
foreach ($fieldList as $field) {
|
||||
$value = $this->metadata->get(['entityAcl', $scope, 'fields', $field, $type]);
|
||||
|
||||
if (!$value && in_array($type, $this->entityDefsTypeList)) {
|
||||
$value = $this->metadata->get(['entityDefs', $scope, 'fields', $field, $type]);
|
||||
}
|
||||
|
||||
if (!$value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isNotEmpty = true;
|
||||
|
||||
$resultFieldList[] = $field;
|
||||
|
||||
$fieldAttributeList = $this->fieldUtil->getAttributeList($scope, $field);
|
||||
|
||||
foreach ($fieldAttributeList as $attribute) {
|
||||
$resultAttributeList[] = $attribute;
|
||||
}
|
||||
}
|
||||
|
||||
$scopeData->fields->$type = $resultFieldList;
|
||||
$scopeData->attributes->$type = $resultAttributeList;
|
||||
}
|
||||
|
||||
foreach ($this->linkTypeList as $type) {
|
||||
$resultLinkList = [];
|
||||
|
||||
foreach ($linkList as $link) {
|
||||
$value = $this->metadata->get(['entityAcl', $scope, 'links', $link, $type]);
|
||||
|
||||
if (!$value && in_array($type, $this->entityDefsTypeList)) {
|
||||
$value = $this->metadata->get(['entityDefs', $scope, 'links', $link, $type]);
|
||||
}
|
||||
|
||||
if (!$value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isNotEmpty = true;
|
||||
|
||||
$resultLinkList[] = $link;
|
||||
}
|
||||
|
||||
$scopeData->links->$type = $resultLinkList;
|
||||
}
|
||||
|
||||
if ($isNotEmpty) {
|
||||
$data->$scope = $scopeData;
|
||||
}
|
||||
}
|
||||
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param self::TYPE_* $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedFieldList(string $scope, string $type): array
|
||||
{
|
||||
assert($this->data !== null);
|
||||
|
||||
if (!property_exists($this->data, $scope)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!property_exists($this->data->$scope, 'fields')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!property_exists($this->data->$scope->fields, $type)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->data->$scope->fields->$type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param self::TYPE_* $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedAttributeList(string $scope, string $type): array
|
||||
{
|
||||
assert($this->data !== null);
|
||||
|
||||
if (!property_exists($this->data, $scope)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!property_exists($this->data->$scope, 'attributes')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!property_exists($this->data->$scope->attributes, $type)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->data->$scope->attributes->$type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param self::TYPE_* $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedLinkList(string $scope, string $type): array
|
||||
{
|
||||
assert($this->data !== null);
|
||||
|
||||
if (!property_exists($this->data, $scope)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!property_exists($this->data->$scope, 'links')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!property_exists($this->data->$scope->links, $type)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->data->$scope->links->$type;
|
||||
}
|
||||
}
|
||||
48
application/Espo/Core/Acl/LinkChecker.php
Normal file
48
application/Espo/Core/Acl/LinkChecker.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\Acl;
|
||||
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* Checks access for linking/unlinking two records.
|
||||
*
|
||||
* @template TEntity of Entity
|
||||
* @template TForeignEntity of Entity
|
||||
*/
|
||||
interface LinkChecker
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @param TForeignEntity $foreignEntity
|
||||
*/
|
||||
public function check(User $user, Entity $entity, Entity $foreignEntity): bool;
|
||||
}
|
||||
74
application/Espo/Core/Acl/LinkChecker/LinkCheckerFactory.php
Normal file
74
application/Espo/Core/Acl/LinkChecker/LinkCheckerFactory.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\Acl\LinkChecker;
|
||||
|
||||
use Espo\Core\Acl\LinkChecker;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
use RuntimeException;
|
||||
|
||||
class LinkCheckerFactory
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a link checker.
|
||||
*
|
||||
* @return LinkChecker<Entity, Entity>
|
||||
*/
|
||||
public function create(string $scope, string $link): LinkChecker
|
||||
{
|
||||
$className = $this->getClassName($scope, $link);
|
||||
|
||||
if (!$className) {
|
||||
throw new RuntimeException("Link checker is not implemented for {$scope}.{$link}.");
|
||||
}
|
||||
|
||||
return $this->injectableFactory->create($className);
|
||||
}
|
||||
|
||||
public function isCreatable(string $scope, string $link): bool
|
||||
{
|
||||
return (bool) $this->getClassName($scope, $link);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?class-string<LinkChecker<Entity, Entity>>
|
||||
*/
|
||||
private function getClassName(string $scope, string $link): ?string
|
||||
{
|
||||
/** @var ?class-string<LinkChecker<Entity, Entity>> */
|
||||
return $this->metadata->get(['aclDefs', $scope, 'linkCheckerClassNameMap', $link]);
|
||||
}
|
||||
}
|
||||
35
application/Espo/Core/Acl/Map/CacheKeyProvider.php
Normal file
35
application/Espo/Core/Acl/Map/CacheKeyProvider.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\Acl\Map;
|
||||
|
||||
interface CacheKeyProvider
|
||||
{
|
||||
public function get(): string;
|
||||
}
|
||||
189
application/Espo/Core/Acl/Map/DataBuilder.php
Normal file
189
application/Espo/Core/Acl/Map/DataBuilder.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?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\Acl\Map;
|
||||
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class DataBuilder
|
||||
{
|
||||
/** @var string[] */
|
||||
private $actionList = [
|
||||
Table::ACTION_READ,
|
||||
Table::ACTION_STREAM,
|
||||
Table::ACTION_EDIT,
|
||||
Table::ACTION_DELETE,
|
||||
Table::ACTION_CREATE,
|
||||
];
|
||||
/** @var string[] */
|
||||
private $fieldActionList = [
|
||||
Table::ACTION_READ,
|
||||
Table::ACTION_EDIT,
|
||||
];
|
||||
/** @var string[] */
|
||||
private $fieldLevelList = [
|
||||
Table::LEVEL_YES,
|
||||
Table::LEVEL_NO,
|
||||
];
|
||||
|
||||
public function __construct(private MetadataProvider $metadataProvider, private FieldUtil $fieldUtil)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @return stdClass&object{table: stdClass, fieldTable: stdClass}
|
||||
*/
|
||||
public function build(Table $table): stdClass
|
||||
{
|
||||
$data = (object) [
|
||||
'table' => (object) [],
|
||||
'fieldTable' => (object) [],
|
||||
];
|
||||
|
||||
foreach ($this->metadataProvider->getScopeList() as $scope) {
|
||||
$data->table->$scope = $this->getScopeRawData($table, $scope);
|
||||
|
||||
$fieldData = $this->getScopeFieldData($table, $scope);
|
||||
|
||||
if ($fieldData !== null) {
|
||||
$data->fieldTable->$scope = $fieldData;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->metadataProvider->getPermissionList() as $permission) {
|
||||
$data->{$permission . 'Permission'} = $table->getPermissionLevel($permission);
|
||||
}
|
||||
|
||||
$data->fieldTableQuickAccess = $this->buildFieldTableQuickAccess($data->fieldTable);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|stdClass
|
||||
*/
|
||||
private function getScopeRawData(Table $table, string $scope)
|
||||
{
|
||||
$data = $table->getScopeData($scope);
|
||||
|
||||
if ($data->isBoolean()) {
|
||||
return $data->isTrue();
|
||||
}
|
||||
|
||||
$rawData = (object) [];
|
||||
|
||||
foreach ($this->actionList as $action) {
|
||||
$rawData->$action = $data->get($action);
|
||||
}
|
||||
|
||||
return $rawData;
|
||||
}
|
||||
|
||||
private function getScopeFieldData(Table $table, string $scope): ?stdClass
|
||||
{
|
||||
if (!$this->metadataProvider->isScopeEntity($scope)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fieldList = $this->metadataProvider->getScopeFieldList($scope);
|
||||
|
||||
$rawData = (object) [];
|
||||
|
||||
foreach ($fieldList as $field) {
|
||||
$data = $table->getFieldData($scope, $field);
|
||||
|
||||
if (
|
||||
$data->getRead() === Table::LEVEL_YES &&
|
||||
$data->getEdit() === Table::LEVEL_YES
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rawData->$field = (object) [
|
||||
Table::ACTION_READ => $data->getRead(),
|
||||
Table::ACTION_EDIT => $data->getEdit(),
|
||||
];
|
||||
}
|
||||
|
||||
return $rawData;
|
||||
}
|
||||
|
||||
protected function buildFieldTableQuickAccess(stdClass $fieldTable): stdClass
|
||||
{
|
||||
$quickAccess = (object) [];
|
||||
|
||||
foreach (get_object_vars($fieldTable) as $scope => $scopeData) {
|
||||
$quickAccess->$scope = $this->buildFieldTableQuickAccessScope($scope, $scopeData);
|
||||
}
|
||||
|
||||
return $quickAccess;
|
||||
}
|
||||
|
||||
private function buildFieldTableQuickAccessScope(string $scope, stdClass $data): stdClass
|
||||
{
|
||||
$quickAccess = (object) [
|
||||
'attributes' => (object) [],
|
||||
'fields' => (object) [],
|
||||
];
|
||||
|
||||
foreach ($this->fieldActionList as $action) {
|
||||
$quickAccess->attributes->$action = (object) [];
|
||||
$quickAccess->fields->$action = (object) [];
|
||||
|
||||
foreach ($this->fieldLevelList as $level) {
|
||||
$quickAccess->attributes->$action->$level = [];
|
||||
$quickAccess->fields->$action->$level = [];
|
||||
}
|
||||
}
|
||||
|
||||
foreach (get_object_vars($data) as $field => $fieldData) {
|
||||
$attributeList = $this->fieldUtil->getAttributeList($scope, $field);
|
||||
|
||||
foreach ($this->fieldActionList as $action) {
|
||||
if (!isset($fieldData->$action)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->fieldLevelList as $level) {
|
||||
if ($fieldData->$action === $level) {
|
||||
$quickAccess->fields->$action->{$level}[] = $field;
|
||||
|
||||
foreach ($attributeList as $attribute) {
|
||||
$quickAccess->attributes->$action->{$level}[] = $attribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $quickAccess;
|
||||
}
|
||||
}
|
||||
43
application/Espo/Core/Acl/Map/DefaultCacheKeyProvider.php
Normal file
43
application/Espo/Core/Acl/Map/DefaultCacheKeyProvider.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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\Acl\Map;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
class DefaultCacheKeyProvider implements CacheKeyProvider
|
||||
{
|
||||
public function __construct(private User $user)
|
||||
{}
|
||||
|
||||
public function get(): string
|
||||
{
|
||||
return 'aclMap/' . $this->user->getId();
|
||||
}
|
||||
}
|
||||
232
application/Espo/Core/Acl/Map/Map.php
Normal file
232
application/Espo/Core/Acl/Map/Map.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?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\Acl\Map;
|
||||
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\DataCache;
|
||||
use Espo\Core\Utils\ObjectUtil;
|
||||
|
||||
use stdClass;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Provides quick access to ACL data.
|
||||
*/
|
||||
class Map
|
||||
{
|
||||
private stdClass $data;
|
||||
private string $cacheKey;
|
||||
/** @var array<string, string[]> */
|
||||
private $forbiddenFieldsCache = [];
|
||||
/** @var array<string, string[]> */
|
||||
private $forbiddenAttributesCache;
|
||||
/** @var string[] */
|
||||
private $fieldLevelList = [
|
||||
Table::LEVEL_YES,
|
||||
Table::LEVEL_NO,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
Table $table,
|
||||
private DataBuilder $dataBuilder,
|
||||
private DataCache $dataCache,
|
||||
CacheKeyProvider $cacheKeyProvider,
|
||||
Config\SystemConfig $systemConfig,
|
||||
) {
|
||||
|
||||
$this->cacheKey = $cacheKeyProvider->get();
|
||||
|
||||
if ($systemConfig->useCache() && $this->dataCache->has($this->cacheKey)) {
|
||||
/** @var stdClass $cachedData */
|
||||
$cachedData = $this->dataCache->get($this->cacheKey);
|
||||
|
||||
$this->data = $cachedData;
|
||||
} else {
|
||||
$this->data = $this->dataBuilder->build($table);
|
||||
|
||||
if ($systemConfig->useCache()) {
|
||||
$this->dataCache->store($this->cacheKey, $this->data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw data (for front-end).
|
||||
*/
|
||||
public function getData(): stdClass
|
||||
{
|
||||
return ObjectUtil::clone($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of forbidden attributes for a scope and action.
|
||||
*
|
||||
* @param string $scope A scope.
|
||||
* @param string $action An action.
|
||||
* @param string $thresholdLevel An attribute will be treated as forbidden if the level is
|
||||
* equal to or lower than the threshold.
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeForbiddenAttributeList(
|
||||
string $scope,
|
||||
string $action = Table::ACTION_READ,
|
||||
string $thresholdLevel = Table::LEVEL_NO
|
||||
): array {
|
||||
|
||||
if (
|
||||
!in_array($thresholdLevel, $this->fieldLevelList) ||
|
||||
$thresholdLevel === Table::LEVEL_YES
|
||||
) {
|
||||
throw new RuntimeException("Bad threshold level.");
|
||||
}
|
||||
|
||||
$key = $scope . '_'. $action . '_' . $thresholdLevel;
|
||||
|
||||
if (isset($this->forbiddenAttributesCache[$key])) {
|
||||
return $this->forbiddenAttributesCache[$key];
|
||||
}
|
||||
|
||||
$fieldTableQuickAccess = $this->data->fieldTableQuickAccess;
|
||||
|
||||
if (
|
||||
!isset($fieldTableQuickAccess->$scope) ||
|
||||
!isset($fieldTableQuickAccess->$scope->attributes) ||
|
||||
!isset($fieldTableQuickAccess->$scope->attributes->$action)
|
||||
) {
|
||||
$this->forbiddenAttributesCache[$key] = [];
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$levelList = [];
|
||||
|
||||
foreach ($this->fieldLevelList as $level) {
|
||||
if (
|
||||
array_search($level, $this->fieldLevelList) >=
|
||||
array_search($thresholdLevel, $this->fieldLevelList)
|
||||
) {
|
||||
$levelList[] = $level;
|
||||
}
|
||||
}
|
||||
|
||||
$attributeList = [];
|
||||
|
||||
foreach ($levelList as $level) {
|
||||
if (!isset($fieldTableQuickAccess->$scope->attributes->$action->$level)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($fieldTableQuickAccess->$scope->attributes->$action->$level as $attribute) {
|
||||
if (in_array($attribute, $attributeList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attributeList[] = $attribute;
|
||||
}
|
||||
}
|
||||
|
||||
$this->forbiddenAttributesCache[$key] = $attributeList;
|
||||
|
||||
return $attributeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of forbidden fields for a scope and action.
|
||||
*
|
||||
* @param string $scope A scope.
|
||||
* @param string $action An action.
|
||||
* @param string $thresholdLevel An attribute will be treated as forbidden if the level is
|
||||
* equal to or lower than the threshold.
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeForbiddenFieldList(
|
||||
string $scope,
|
||||
string $action = Table::ACTION_READ,
|
||||
string $thresholdLevel = Table::LEVEL_NO
|
||||
): array {
|
||||
|
||||
if (
|
||||
!in_array($thresholdLevel, $this->fieldLevelList) ||
|
||||
$thresholdLevel === Table::LEVEL_YES
|
||||
) {
|
||||
throw new RuntimeException("Bad threshold level.");
|
||||
}
|
||||
|
||||
$key = $scope . '_'. $action . '_' . $thresholdLevel;
|
||||
|
||||
if (isset($this->forbiddenFieldsCache[$key])) {
|
||||
return $this->forbiddenFieldsCache[$key];
|
||||
}
|
||||
|
||||
$fieldTableQuickAccess = $this->data->fieldTableQuickAccess;
|
||||
|
||||
if (
|
||||
!isset($fieldTableQuickAccess->$scope) ||
|
||||
!isset($fieldTableQuickAccess->$scope->fields) ||
|
||||
!isset($fieldTableQuickAccess->$scope->fields->$action)
|
||||
) {
|
||||
$this->forbiddenFieldsCache[$key] = [];
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$levelList = [];
|
||||
|
||||
foreach ($this->fieldLevelList as $level) {
|
||||
if (
|
||||
array_search($level, $this->fieldLevelList) >=
|
||||
array_search($thresholdLevel, $this->fieldLevelList)
|
||||
) {
|
||||
$levelList[] = $level;
|
||||
}
|
||||
}
|
||||
|
||||
$fieldList = [];
|
||||
|
||||
foreach ($levelList as $level) {
|
||||
if (!isset($fieldTableQuickAccess->$scope->fields->$action->$level)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($fieldTableQuickAccess->$scope->fields->$action->$level as $field) {
|
||||
if (in_array($field, $fieldList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldList[] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
$this->forbiddenFieldsCache[$key] = $fieldList;
|
||||
|
||||
return $fieldList;
|
||||
}
|
||||
}
|
||||
64
application/Espo/Core/Acl/Map/MapFactory.php
Normal file
64
application/Espo/Core/Acl/Map/MapFactory.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?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\Acl\Map;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Binding\Binder;
|
||||
use Espo\Core\Binding\BindingContainer;
|
||||
use Espo\Core\Binding\BindingData;
|
||||
use Espo\Core\InjectableFactory;
|
||||
|
||||
class MapFactory
|
||||
{
|
||||
public function __construct(private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
public function create(User $user, Table $table): Map
|
||||
{
|
||||
$bindingContainer = $this->createBindingContainer($user, $table);
|
||||
|
||||
return $this->injectableFactory->createWithBinding(Map::class, $bindingContainer);
|
||||
}
|
||||
|
||||
private function createBindingContainer(User $user, Table $table): BindingContainer
|
||||
{
|
||||
$bindingData = new BindingData();
|
||||
|
||||
$binder = new Binder($bindingData);
|
||||
$binder
|
||||
->bindInstance(User::class, $user)
|
||||
->bindInstance(Table::class, $table)
|
||||
->bindImplementation(CacheKeyProvider::class, DefaultCacheKeyProvider::class);
|
||||
|
||||
return new BindingContainer($bindingData);
|
||||
}
|
||||
}
|
||||
82
application/Espo/Core/Acl/Map/MetadataProvider.php
Normal file
82
application/Espo/Core/Acl/Map/MetadataProvider.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?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\Acl\Map;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
class MetadataProvider
|
||||
{
|
||||
protected string $type = 'acl';
|
||||
|
||||
public function __construct(private Metadata $metadata)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeList(): array
|
||||
{
|
||||
/** @var string[] */
|
||||
return array_keys($this->metadata->get('scopes') ?? []);
|
||||
}
|
||||
|
||||
public function isScopeEntity(string $scope): bool
|
||||
{
|
||||
return (bool) $this->metadata->get(['scopes', $scope, 'entity']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeFieldList(string $scope): array
|
||||
{
|
||||
/** @var string[] */
|
||||
return array_keys($this->metadata->get(['entityDefs', $scope, 'fields']) ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getPermissionList(): array
|
||||
{
|
||||
$itemList = $this->metadata->get(['app', $this->type, 'valuePermissionList']) ?? [];
|
||||
|
||||
return array_map(
|
||||
function (string $item): string {
|
||||
if (str_ends_with($item, 'Permission')) {
|
||||
return substr($item, 0, -10);
|
||||
}
|
||||
|
||||
return $item;
|
||||
},
|
||||
$itemList
|
||||
);
|
||||
}
|
||||
}
|
||||
90
application/Espo/Core/Acl/OwnerUserFieldProvider.php
Normal file
90
application/Espo/Core/Acl/OwnerUserFieldProvider.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?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\Acl;
|
||||
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Defs;
|
||||
|
||||
class OwnerUserFieldProvider
|
||||
{
|
||||
protected const FIELD_ASSIGNED_USERS = Field::ASSIGNED_USERS;
|
||||
protected const FIELD_ASSIGNED_USER = Field::ASSIGNED_USER;
|
||||
protected const FIELD_CREATED_BY = Field::CREATED_BY;
|
||||
|
||||
public function __construct(private Defs $ormDefs, private Metadata $metadata)
|
||||
{}
|
||||
|
||||
/**
|
||||
* Get an entity field that stores an owner-user (or multiple users).
|
||||
* Must be a link or linkMultiple field. NULL means no owner.
|
||||
*/
|
||||
public function get(string $entityType): ?string
|
||||
{
|
||||
$value = $this->metadata->get(['aclDefs', $entityType, 'readOwnerUserField']);
|
||||
|
||||
if ($value) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$defs = $this->ormDefs->getEntity($entityType);
|
||||
|
||||
if (
|
||||
$defs->hasField(self::FIELD_ASSIGNED_USERS) &&
|
||||
$defs->getField(self::FIELD_ASSIGNED_USERS)->getType() === FieldType::LINK_MULTIPLE &&
|
||||
$defs->hasRelation(self::FIELD_ASSIGNED_USERS) &&
|
||||
$defs->getRelation(self::FIELD_ASSIGNED_USERS)->getForeignEntityType() === User::ENTITY_TYPE
|
||||
) {
|
||||
return self::FIELD_ASSIGNED_USERS;
|
||||
}
|
||||
|
||||
if (
|
||||
$defs->hasField(self::FIELD_ASSIGNED_USER) &&
|
||||
$defs->getField(self::FIELD_ASSIGNED_USER)->getType() === FieldType::LINK &&
|
||||
$defs->hasRelation(self::FIELD_ASSIGNED_USER) &&
|
||||
$defs->getRelation(self::FIELD_ASSIGNED_USER)->getForeignEntityType() === User::ENTITY_TYPE
|
||||
) {
|
||||
return self::FIELD_ASSIGNED_USER;
|
||||
}
|
||||
|
||||
if (
|
||||
$defs->hasField(self::FIELD_CREATED_BY) &&
|
||||
$defs->getField(self::FIELD_CREATED_BY)->getType() === FieldType::LINK &&
|
||||
$defs->hasRelation(self::FIELD_CREATED_BY) &&
|
||||
$defs->getRelation(self::FIELD_CREATED_BY)->getForeignEntityType() === User::ENTITY_TYPE
|
||||
) {
|
||||
return self::FIELD_CREATED_BY;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
37
application/Espo/Core/Acl/OwnershipChecker.php
Normal file
37
application/Espo/Core/Acl/OwnershipChecker.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?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\Acl;
|
||||
|
||||
/**
|
||||
* Bindings:
|
||||
* - `$entityType` – as of v9.1.0.
|
||||
* - `Espo\Core\AclManager`
|
||||
*/
|
||||
interface OwnershipChecker {}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?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\Acl\OwnershipChecker;
|
||||
|
||||
use Espo\Core\Acl\DefaultOwnershipChecker;
|
||||
use Espo\Core\Acl\Exceptions\NotImplemented;
|
||||
use Espo\Core\Acl\OwnershipChecker;
|
||||
use Espo\Core\AclManager;
|
||||
use Espo\Core\Binding\Binder;
|
||||
use Espo\Core\Binding\BindingContainer;
|
||||
use Espo\Core\Binding\BindingData;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
class OwnershipCheckerFactory
|
||||
{
|
||||
/** @var class-string<OwnershipChecker> */
|
||||
private string $defaultClassName = DefaultOwnershipChecker::class;
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create an ownership checker.
|
||||
*
|
||||
* @throws NotImplemented
|
||||
*/
|
||||
public function create(string $scope, AclManager $aclManager): OwnershipChecker
|
||||
{
|
||||
$className = $this->getClassName($scope);
|
||||
|
||||
$bindingContainer = $this->createBindingContainer($className, $aclManager, $scope);
|
||||
|
||||
return $this->injectableFactory->createWithBinding($className, $bindingContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<OwnershipChecker>
|
||||
* @throws NotImplemented
|
||||
*/
|
||||
private function getClassName(string $scope): string
|
||||
{
|
||||
/** @var ?class-string<OwnershipChecker> $className */
|
||||
$className = $this->metadata->get(['aclDefs', $scope, 'ownershipCheckerClassName']);
|
||||
|
||||
if ($className) {
|
||||
return $className;
|
||||
}
|
||||
|
||||
if (!$this->metadata->get(['scopes', $scope])) {
|
||||
throw new NotImplemented();
|
||||
}
|
||||
|
||||
return $this->defaultClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<OwnershipChecker> $className
|
||||
*/
|
||||
private function createBindingContainer(string $className, AclManager $aclManager, string $scope): BindingContainer
|
||||
{
|
||||
$bindingData = new BindingData();
|
||||
|
||||
$binder = new Binder($bindingData);
|
||||
|
||||
$binder->bindInstance(AclManager::class, $aclManager);
|
||||
|
||||
$binder
|
||||
->for($className)
|
||||
->bindValue('$entityType', $scope);
|
||||
|
||||
return new BindingContainer($bindingData);
|
||||
}
|
||||
}
|
||||
46
application/Espo/Core/Acl/OwnershipOwnChecker.php
Normal file
46
application/Espo/Core/Acl/OwnershipOwnChecker.php
Normal file
@@ -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\Acl;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface OwnershipOwnChecker extends OwnershipChecker
|
||||
{
|
||||
/**
|
||||
* Check whether a user is an owner of an entity.
|
||||
*
|
||||
* @param TEntity $entity
|
||||
*/
|
||||
public function checkOwn(User $user, Entity $entity): bool;
|
||||
}
|
||||
48
application/Espo/Core/Acl/OwnershipSharedChecker.php
Normal file
48
application/Espo/Core/Acl/OwnershipSharedChecker.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\Acl;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface OwnershipSharedChecker extends OwnershipChecker
|
||||
{
|
||||
/**
|
||||
* Check whether an entity is shared with a user.
|
||||
*
|
||||
* @param TEntity $entity
|
||||
* @param Table::ACTION_* $action
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function checkShared(User $user, Entity $entity, string $action): bool;
|
||||
}
|
||||
46
application/Espo/Core/Acl/OwnershipTeamChecker.php
Normal file
46
application/Espo/Core/Acl/OwnershipTeamChecker.php
Normal file
@@ -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\Acl;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface OwnershipTeamChecker extends OwnershipChecker
|
||||
{
|
||||
/**
|
||||
* Check whether an entity belongs to a user team.
|
||||
*
|
||||
* @param TEntity $entity
|
||||
*/
|
||||
public function checkTeam(User $user, Entity $entity): bool;
|
||||
}
|
||||
46
application/Espo/Core/Acl/Permission.php
Normal file
46
application/Espo/Core/Acl/Permission.php
Normal file
@@ -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\Acl;
|
||||
|
||||
class Permission
|
||||
{
|
||||
public const ASSIGNMENT = 'assignment';
|
||||
public const USER = 'user';
|
||||
public const PORTAL = 'portal';
|
||||
public const MASS_UPDATE = 'massUpdate';
|
||||
public const EXPORT = 'export';
|
||||
public const AUDIT = 'audit';
|
||||
public const DATA_PRIVACY = 'dataPrivacy';
|
||||
public const MESSAGE = 'message';
|
||||
public const MENTION = 'mention';
|
||||
public const USER_CALENDAR = 'userCalendar';
|
||||
public const FOLLOWER_MANAGEMENT = 'followerManagement';
|
||||
public const GROUP_EMAIL_ACCOUNT = 'groupEmailAccount';
|
||||
}
|
||||
183
application/Espo/Core/Acl/ScopeData.php
Normal file
183
application/Espo/Core/Acl/ScopeData.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?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\Acl;
|
||||
|
||||
use stdClass;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Scope data.
|
||||
*/
|
||||
class ScopeData
|
||||
{
|
||||
/** @var stdClass|bool */
|
||||
private $raw;
|
||||
/** @var array<string, string> */
|
||||
private $actionData = [];
|
||||
private bool $isBoolean = false;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
/**
|
||||
* @return never
|
||||
*/
|
||||
public function __get(string $name)
|
||||
{
|
||||
throw new RuntimeException("Accessing ScopeData properties is not allowed.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Is of boolean type.
|
||||
*/
|
||||
public function isBoolean(): bool
|
||||
{
|
||||
return $this->isBoolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is true.
|
||||
*/
|
||||
public function isTrue(): bool
|
||||
{
|
||||
if (!$this->isBoolean) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->raw === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is false.
|
||||
*/
|
||||
public function isFalse(): bool
|
||||
{
|
||||
if (!$this->isBoolean) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->raw === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has any level other than 'no'.
|
||||
*/
|
||||
public function hasNotNo(): bool
|
||||
{
|
||||
foreach ($this->actionData as $level) {
|
||||
if ($level !== Table::LEVEL_NO) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a level for an action.
|
||||
*/
|
||||
public function get(string $action): string
|
||||
{
|
||||
return $this->actionData[$action] ?? Table::LEVEL_NO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a 'read' level.
|
||||
*/
|
||||
public function getRead(): string
|
||||
{
|
||||
return $this->get(Table::ACTION_READ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a 'stream' level.
|
||||
*/
|
||||
public function getStream(): string
|
||||
{
|
||||
return $this->get(Table::ACTION_STREAM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a 'create' level.
|
||||
*/
|
||||
public function getCreate(): string
|
||||
{
|
||||
return $this->get(Table::ACTION_CREATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an 'edit' level.
|
||||
*/
|
||||
public function getEdit(): string
|
||||
{
|
||||
return $this->get(Table::ACTION_EDIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a 'delete' level.
|
||||
*/
|
||||
public function getDelete(): string
|
||||
{
|
||||
return $this->get(Table::ACTION_DELETE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a raw table value.
|
||||
*
|
||||
* @param stdClass|bool $raw
|
||||
* @return self
|
||||
*/
|
||||
public static function fromRaw($raw): self
|
||||
{
|
||||
/** @var mixed $raw */
|
||||
|
||||
$obj = new self();
|
||||
|
||||
if ($raw instanceof stdClass) {
|
||||
$obj->isBoolean = false;
|
||||
|
||||
$obj->actionData = get_object_vars($raw);
|
||||
|
||||
foreach ($obj->actionData as $item) {
|
||||
if (!is_string($item)) {
|
||||
throw new RuntimeException("Bad raw scope data.");
|
||||
}
|
||||
}
|
||||
} else if (is_bool($raw)) {
|
||||
$obj->isBoolean = true;
|
||||
} else {
|
||||
throw new InvalidArgumentException();
|
||||
}
|
||||
|
||||
$obj->raw = $raw;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
65
application/Espo/Core/Acl/Table.php
Normal file
65
application/Espo/Core/Acl/Table.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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\Acl;
|
||||
|
||||
/**
|
||||
* Access levels for a user.
|
||||
*/
|
||||
interface Table
|
||||
{
|
||||
public const LEVEL_YES = 'yes';
|
||||
public const LEVEL_NO = 'no';
|
||||
public const LEVEL_ALL = 'all';
|
||||
public const LEVEL_TEAM = 'team';
|
||||
public const LEVEL_OWN = 'own';
|
||||
|
||||
public const ACTION_READ = 'read';
|
||||
public const ACTION_STREAM = 'stream';
|
||||
public const ACTION_EDIT = 'edit';
|
||||
public const ACTION_DELETE = 'delete';
|
||||
public const ACTION_CREATE = 'create';
|
||||
|
||||
/**
|
||||
* Get scope data.
|
||||
*/
|
||||
public function getScopeData(string $scope): ScopeData;
|
||||
|
||||
/**
|
||||
* Get field data.
|
||||
*/
|
||||
public function getFieldData(string $scope, string $field): FieldData;
|
||||
|
||||
/**
|
||||
* Get a permission level.
|
||||
*
|
||||
* @return self::ACTION_*
|
||||
*/
|
||||
public function getPermissionLevel(string $permission): string;
|
||||
}
|
||||
35
application/Espo/Core/Acl/Table/CacheKeyProvider.php
Normal file
35
application/Espo/Core/Acl/Table/CacheKeyProvider.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\Acl\Table;
|
||||
|
||||
interface CacheKeyProvider
|
||||
{
|
||||
public function get(): string;
|
||||
}
|
||||
47
application/Espo/Core/Acl/Table/DefaultCacheKeyProvider.php
Normal file
47
application/Espo/Core/Acl/Table/DefaultCacheKeyProvider.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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\Acl\Table;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
class DefaultCacheKeyProvider implements CacheKeyProvider
|
||||
{
|
||||
private $user;
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function get(): string
|
||||
{
|
||||
return 'acl/' . $this->user->getId();
|
||||
}
|
||||
}
|
||||
103
application/Espo/Core/Acl/Table/DefaultRoleListProvider.php
Normal file
103
application/Espo/Core/Acl/Table/DefaultRoleListProvider.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?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\Acl\Table;
|
||||
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Entities\Team;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Entities\Role as RoleEntity;
|
||||
|
||||
class DefaultRoleListProvider implements RoleListProvider
|
||||
{
|
||||
private const PARAM_BASELINE_ROLE_ID = 'baselineRoleId';
|
||||
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private EntityManager $entityManager,
|
||||
private Config $config,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return Role[]
|
||||
*/
|
||||
public function get(): array
|
||||
{
|
||||
$roleList = [];
|
||||
|
||||
$baselineRole = $this->getBaselineRole();
|
||||
|
||||
if ($baselineRole) {
|
||||
$roleList[] = $baselineRole;
|
||||
}
|
||||
|
||||
/** @var iterable<RoleEntity> $userRoleList */
|
||||
$userRoleList = $this->entityManager
|
||||
->getRelation($this->user, User::LINK_ROLES)
|
||||
->find();
|
||||
|
||||
foreach ($userRoleList as $role) {
|
||||
$roleList[] = $role;
|
||||
}
|
||||
|
||||
/** @var iterable<Team> $teamList */
|
||||
$teamList = $this->entityManager
|
||||
->getRelation($this->user, Field::TEAMS)
|
||||
->find();
|
||||
|
||||
foreach ($teamList as $team) {
|
||||
/** @var iterable<RoleEntity> $teamRoleList */
|
||||
$teamRoleList = $this->entityManager
|
||||
->getRelation($team, Team::LINK_ROLES)
|
||||
->find();
|
||||
|
||||
foreach ($teamRoleList as $role) {
|
||||
$roleList[] = $role;
|
||||
}
|
||||
}
|
||||
|
||||
return array_map(
|
||||
fn (RoleEntity $role) => new RoleEntityWrapper($role),
|
||||
$roleList
|
||||
);
|
||||
}
|
||||
|
||||
private function getBaselineRole(): ?RoleEntity
|
||||
{
|
||||
$roleId = $this->config->get(self::PARAM_BASELINE_ROLE_ID);
|
||||
|
||||
if (!$roleId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->entityManager->getRDBRepositoryByClass(RoleEntity::class)->getById($roleId);
|
||||
}
|
||||
}
|
||||
699
application/Espo/Core/Acl/Table/DefaultTable.php
Normal file
699
application/Espo/Core/Acl/Table/DefaultTable.php
Normal file
@@ -0,0 +1,699 @@
|
||||
<?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\Acl\Table;
|
||||
|
||||
use Espo\Core\Utils\Config\SystemConfig;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\Core\Acl\FieldData;
|
||||
use Espo\Core\Acl\ScopeData;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Utils\DataCache;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use stdClass;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* A table is generated for a user. Multiple roles are merged into a single table.
|
||||
* Stores access levels.
|
||||
*/
|
||||
class DefaultTable implements Table
|
||||
{
|
||||
private const LEVEL_NOT_SET = 'not-set';
|
||||
|
||||
protected string $type = 'acl';
|
||||
|
||||
/** @var string[] */
|
||||
private $actionList = [
|
||||
self::ACTION_READ,
|
||||
self::ACTION_STREAM,
|
||||
self::ACTION_EDIT,
|
||||
self::ACTION_DELETE,
|
||||
self::ACTION_CREATE,
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private $booleanActionList = [
|
||||
self::ACTION_CREATE,
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
protected $levelList = [
|
||||
self::LEVEL_YES,
|
||||
self::LEVEL_ALL,
|
||||
self::LEVEL_TEAM,
|
||||
self::LEVEL_OWN,
|
||||
self::LEVEL_NO,
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private $fieldActionList = [
|
||||
self::ACTION_READ,
|
||||
self::ACTION_EDIT,
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
protected $fieldLevelList = [
|
||||
self::LEVEL_YES,
|
||||
self::LEVEL_NO,
|
||||
];
|
||||
|
||||
private stdClass $data;
|
||||
private string $cacheKey;
|
||||
/** @var string[] */
|
||||
private $valuePermissionList = [];
|
||||
private ScopeDataResolver $scopeDataResolver;
|
||||
|
||||
public function __construct(
|
||||
private RoleListProvider $roleListProvider,
|
||||
CacheKeyProvider $cacheKeyProvider,
|
||||
protected User $user,
|
||||
SystemConfig $systemConfig,
|
||||
protected Metadata $metadata,
|
||||
DataCache $dataCache,
|
||||
) {
|
||||
|
||||
$this->data = (object) [
|
||||
'scopes' => (object) [],
|
||||
'fields' => (object) [],
|
||||
'permissions' => (object) [],
|
||||
];
|
||||
|
||||
if (!$this->user->isFetched()) {
|
||||
throw new RuntimeException('User must be fetched before ACL check.');
|
||||
}
|
||||
|
||||
$this->valuePermissionList = $this->metadata
|
||||
->get(['app', $this->type, 'valuePermissionList'], []);
|
||||
|
||||
$this->cacheKey = $cacheKeyProvider->get();
|
||||
|
||||
if ($systemConfig->useCache() && $dataCache->has($this->cacheKey)) {
|
||||
/** @var stdClass $cachedData */
|
||||
$cachedData = $dataCache->get($this->cacheKey);
|
||||
|
||||
$this->data = $cachedData;
|
||||
} else {
|
||||
$this->load();
|
||||
|
||||
if ($systemConfig->useCache()) {
|
||||
$dataCache->store($this->cacheKey, $this->data);
|
||||
}
|
||||
}
|
||||
|
||||
$this->scopeDataResolver = new ScopeDataResolver($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scope data.
|
||||
*/
|
||||
public function getScopeData(string $scope): ScopeData
|
||||
{
|
||||
if (!isset($this->data->scopes->$scope)) {
|
||||
return ScopeData::fromRaw(false);
|
||||
}
|
||||
|
||||
$data = $this->data->scopes->$scope;
|
||||
|
||||
return $this->scopeDataResolver->resolve($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field data.
|
||||
*/
|
||||
public function getFieldData(string $scope, string $field): FieldData
|
||||
{
|
||||
if (!isset($this->data->fields->$scope)) {
|
||||
return FieldData::fromRaw((object) [
|
||||
self::ACTION_READ => self::LEVEL_YES,
|
||||
self::ACTION_EDIT => self::LEVEL_YES,
|
||||
]);
|
||||
}
|
||||
|
||||
$data = $this->data->fields->$scope->$field ?? (object) [
|
||||
self::ACTION_READ => self::LEVEL_YES,
|
||||
self::ACTION_EDIT => self::LEVEL_YES,
|
||||
];
|
||||
|
||||
return FieldData::fromRaw($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a permission level.
|
||||
*/
|
||||
public function getPermissionLevel(string $permission): string
|
||||
{
|
||||
return $this->data->permissions->$permission ?? self::LEVEL_NO;
|
||||
}
|
||||
|
||||
private function load(): void
|
||||
{
|
||||
$valuePermissionLists = (object) [];
|
||||
|
||||
foreach ($this->valuePermissionList as $permission) {
|
||||
$valuePermissionLists->$permission = [];
|
||||
}
|
||||
|
||||
$aclTableList = [];
|
||||
$fieldTableList = [];
|
||||
|
||||
$aclTable = (object) [];
|
||||
$fieldTable = (object) [];
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
$roleList = $this->roleListProvider->get();
|
||||
|
||||
foreach ($roleList as $role) {
|
||||
$aclTableList[] = $role->getScopeTableData();
|
||||
$fieldTableList[] = $role->getFieldTableData();
|
||||
|
||||
foreach ($this->valuePermissionList as $permissionKey) {
|
||||
$permission = $this->normalizePermissionName($permissionKey);
|
||||
|
||||
$valuePermissionLists->{$permissionKey}[] = $role->getPermissionLevel($permission);
|
||||
}
|
||||
}
|
||||
|
||||
$aclTable = $this->mergeTableList($aclTableList);
|
||||
$fieldTable = $this->mergeFieldTableList($fieldTableList);
|
||||
|
||||
$this->applyDefault($aclTable, $fieldTable);
|
||||
$this->applyDisabled($aclTable, $fieldTable);
|
||||
$this->applyMandatory($aclTable, $fieldTable);
|
||||
}
|
||||
|
||||
if ($this->user->isAdmin()) {
|
||||
$aclTable = (object) [];
|
||||
$fieldTable = (object) [];
|
||||
|
||||
$this->applyHighest($aclTable);
|
||||
$this->applyDisabled($aclTable, $fieldTable);
|
||||
$this->applyAdminMandatory($aclTable, $fieldTable);
|
||||
}
|
||||
|
||||
foreach (get_object_vars($aclTable) as $scope => $data) {
|
||||
if (is_string($data) && isset($aclTable->$data)) {
|
||||
$aclTable->$scope = $aclTable->$data;
|
||||
}
|
||||
}
|
||||
|
||||
$this->data->scopes = $aclTable;
|
||||
$this->data->fields = $fieldTable;
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
foreach ($this->valuePermissionList as $permissionKey) {
|
||||
$permission = $this->normalizePermissionName($permissionKey);
|
||||
|
||||
$defaultLevel = $this->metadata
|
||||
->get(['app', $this->type, 'permissionsStrictDefaults', $permissionKey]) ??
|
||||
self::LEVEL_NO;
|
||||
|
||||
$this->data->permissions->$permission = $this->mergeValueList(
|
||||
$valuePermissionLists->$permissionKey,
|
||||
$defaultLevel
|
||||
);
|
||||
|
||||
$mandatoryLevel = $this->metadata->get(['app', $this->type, 'mandatory', $permissionKey]);
|
||||
|
||||
if ($mandatoryLevel !== null) {
|
||||
$this->data->permissions->$permission = $mandatoryLevel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->user->isAdmin()) {
|
||||
foreach ($this->valuePermissionList as $permissionKey) {
|
||||
$permission = $this->normalizePermissionName($permissionKey);
|
||||
|
||||
$highestLevel = $this->metadata
|
||||
->get(['app', $this->type, 'valuePermissionHighestLevels', $permissionKey]);
|
||||
|
||||
if ($highestLevel !== null) {
|
||||
$this->data->permissions->$permission = $highestLevel;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->data->permissions->$permission = self::LEVEL_ALL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizePermissionName(string $permissionKey): string
|
||||
{
|
||||
$permission = $permissionKey;
|
||||
|
||||
if (str_ends_with($permissionKey, 'Permission')) {
|
||||
$permission = substr($permissionKey, 0, -10);
|
||||
}
|
||||
|
||||
return $permission;
|
||||
}
|
||||
|
||||
private function applyHighest(stdClass $table): void
|
||||
{
|
||||
foreach ($this->getScopeList() as $scope) {
|
||||
if ($this->metadata->get(['scopes', $scope, $this->type]) === ScopeDataType::BOOLEAN) {
|
||||
$table->$scope = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->metadata->get(['scopes', $scope, 'entity'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$table->$scope = (object) [];
|
||||
|
||||
$actionList = $this->metadata->get(
|
||||
['scopes', $scope, $this->type . 'ActionList'],
|
||||
$this->actionList
|
||||
);
|
||||
|
||||
$highest = $this->metadata->get(
|
||||
['scopes', $scope, $this->type . 'HighestLevel'],
|
||||
self::LEVEL_ALL
|
||||
);
|
||||
|
||||
foreach ($actionList as $action) {
|
||||
$table->$scope->$action = $highest;
|
||||
|
||||
if (in_array($action, $this->booleanActionList)) {
|
||||
$table->$scope->$action = self::LEVEL_YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function applyDefault(stdClass &$table, stdClass &$fieldTable): void
|
||||
{
|
||||
if ($this->user->isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = $this->metadata->get(['app', $this->type, 'strictDefault', 'scopeLevel'], []);
|
||||
|
||||
foreach ($data as $scope => $item) {
|
||||
if (isset($table->$scope)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $item;
|
||||
|
||||
if (is_array($item)) {
|
||||
$value = (object) $item;
|
||||
}
|
||||
|
||||
$table->$scope = $value;
|
||||
}
|
||||
|
||||
$defaultFieldData = $this->metadata
|
||||
->get(['app', $this->type, 'strictDefault', 'fieldLevel']) ?? [];
|
||||
|
||||
foreach ($this->getScopeList() as $scope) {
|
||||
if (isset($table->$scope) && $table->$scope === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->metadata->get(['scopes', $scope, 'entity'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldList = array_keys($this->metadata->get(['entityDefs', $scope, 'fields']) ?? []);
|
||||
|
||||
$defaultScopeFieldData = $this->metadata
|
||||
->get(['app', $this->type, 'strictDefault', 'scopeFieldLevel', $scope]) ?? [];
|
||||
|
||||
foreach (array_merge($defaultFieldData, $defaultScopeFieldData) as $field => $f) {
|
||||
if (!in_array($field, $fieldList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($fieldTable->$scope)) {
|
||||
$fieldTable->$scope = (object) [];
|
||||
}
|
||||
|
||||
if (isset($fieldTable->$scope->$field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldTable->$scope->$field = (object) [];
|
||||
|
||||
foreach ($this->fieldActionList as $action) {
|
||||
$level = self::LEVEL_NO;
|
||||
|
||||
if ($f === true) {
|
||||
$level = self::LEVEL_YES;
|
||||
} else {
|
||||
if (is_array($f) && isset($f[$action])) {
|
||||
$level = $f[$action];
|
||||
}
|
||||
}
|
||||
|
||||
$fieldTable->$scope->$field->$action = $level;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->getScopeWithAclList() as $scope) {
|
||||
if (isset($table->$scope)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$aclType = $this->metadata->get(['scopes', $scope, $this->type]);
|
||||
|
||||
if (empty($aclType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$table->$scope = false;
|
||||
}
|
||||
}
|
||||
|
||||
private function applyMandatoryInternal(stdClass $table, stdClass $fieldTable, string $type): void
|
||||
{
|
||||
$data = $this->metadata->get(['app', $this->type, $type, 'scopeLevel']) ?? [];
|
||||
|
||||
foreach ($data as $scope => $item) {
|
||||
$value = $item;
|
||||
|
||||
if (is_array($item)) {
|
||||
$value = (object) $item;
|
||||
}
|
||||
|
||||
$table->$scope = $value;
|
||||
}
|
||||
|
||||
$mandatoryFieldData = $this->metadata->get(['app', $this->type, $type, 'fieldLevel']) ?? [];
|
||||
|
||||
foreach ($this->getScopeList() as $scope) {
|
||||
if (isset($table->$scope) && $table->$scope === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->metadata->get(['scopes', $scope, 'entity'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldList = array_keys($this->metadata->get(['entityDefs', $scope, 'fields']) ?? []);
|
||||
|
||||
$mandatoryScopeFieldData = $this->metadata
|
||||
->get(['app', $this->type, $type, 'scopeFieldLevel', $scope]) ?? [];
|
||||
|
||||
foreach (array_merge($mandatoryFieldData, $mandatoryScopeFieldData) as $field => $item) {
|
||||
if (!in_array($field, $fieldList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($fieldTable->$scope)) {
|
||||
$fieldTable->$scope = (object) [];
|
||||
}
|
||||
|
||||
$fieldTable->$scope->$field = (object) [];
|
||||
|
||||
foreach ($this->fieldActionList as $action) {
|
||||
$level = self::LEVEL_NO;
|
||||
|
||||
if ($item === true) {
|
||||
$level = self::LEVEL_YES;
|
||||
} else {
|
||||
if (is_array($item) && isset($item[$action])) {
|
||||
$level = $item[$action];
|
||||
}
|
||||
}
|
||||
|
||||
$fieldTable->$scope->$field->$action = $level;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function applyMandatory(stdClass $table, stdClass $fieldTable): void
|
||||
{
|
||||
$this->applyMandatoryInternal($table, $fieldTable, 'mandatory');
|
||||
}
|
||||
|
||||
private function applyAdminMandatory(stdClass $table, stdClass $fieldTable): void
|
||||
{
|
||||
$this->applyMandatoryInternal($table, $fieldTable, 'adminMandatory');
|
||||
}
|
||||
|
||||
protected function applyDisabled(stdClass $table, stdClass $fieldTable): void
|
||||
{
|
||||
foreach ($this->getScopeList() as $scope) {
|
||||
if ($this->metadata->get(['scopes', $scope, 'disabled'])) {
|
||||
$table->$scope = false;
|
||||
|
||||
unset($fieldTable->$scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $list
|
||||
*/
|
||||
private function mergeValueList(array $list, string $defaultValue): string
|
||||
{
|
||||
$result = null;
|
||||
|
||||
foreach ($list as $level) {
|
||||
if ($level === self::LEVEL_NOT_SET) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_null($result)) {
|
||||
$result = $level;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
array_search($result, $this->levelList) >
|
||||
array_search($level, $this->levelList)
|
||||
) {
|
||||
$result = $level;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($result)) {
|
||||
$result = $defaultValue;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getScopeWithAclList(): array
|
||||
{
|
||||
$scopeList = [];
|
||||
|
||||
$scopes = $this->metadata->get('scopes');
|
||||
|
||||
foreach ($scopes as $scope => $d) {
|
||||
if (empty($d['acl'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$scopeList[] = $scope;
|
||||
}
|
||||
|
||||
return $scopeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getScopeList(): array
|
||||
{
|
||||
$scopeList = [];
|
||||
|
||||
$scopes = $this->metadata->get('scopes');
|
||||
|
||||
foreach ($scopes as $scope => $item) {
|
||||
$scopeList[] = $scope;
|
||||
}
|
||||
|
||||
return $scopeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass[] $tableList
|
||||
*/
|
||||
private function mergeTableList(array $tableList): stdClass
|
||||
{
|
||||
$data = (object) [];
|
||||
|
||||
$scopeList = $this->getScopeWithAclList();
|
||||
|
||||
foreach ($tableList as $table) {
|
||||
foreach ($scopeList as $scope) {
|
||||
if (!isset($table->$scope)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->mergeTableListItem($data, $scope, $table->$scope);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass|bool|null $row
|
||||
*/
|
||||
private function mergeTableListItem(stdClass $data, string $scope, $row): void
|
||||
{
|
||||
if ($row === false || $row === null) {
|
||||
if (!isset($data->$scope)) {
|
||||
$data->$scope = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($row === true) {
|
||||
$data->$scope = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($data->$scope)) {
|
||||
$data->$scope = (object) [];
|
||||
}
|
||||
|
||||
if ($data->$scope === false) {
|
||||
$data->$scope = (object) [];
|
||||
}
|
||||
|
||||
if (!is_object($row)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actionList = $this->metadata
|
||||
->get(['scopes', $scope, $this->type . 'ActionList']) ?? $this->actionList;
|
||||
|
||||
foreach ($actionList as $i => $action) {
|
||||
if (isset($row->$action)) {
|
||||
$level = $row->$action;
|
||||
|
||||
if (!isset($data->$scope->$action)) {
|
||||
$data->$scope->$action = $level;
|
||||
} else if (
|
||||
array_search($data->$scope->$action, $this->levelList) >
|
||||
array_search($level, $this->levelList)
|
||||
) {
|
||||
$data->$scope->$action = $level;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($i === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// @todo Remove everything below.
|
||||
$previousAction = $this->actionList[$i - 1];
|
||||
|
||||
if (in_array($action, $this->booleanActionList)) {
|
||||
$data->$scope->$action = self::LEVEL_YES;
|
||||
} else if ($action === self::ACTION_STREAM && isset($data->$scope->$previousAction)) {
|
||||
$data->$scope->$action = $data->$scope->$previousAction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass[] $tableList
|
||||
*/
|
||||
private function mergeFieldTableList(array $tableList): stdClass
|
||||
{
|
||||
$data = (object) [];
|
||||
|
||||
$scopeList = $this->getScopeWithAclList();
|
||||
|
||||
foreach ($tableList as $table) {
|
||||
foreach ($scopeList as $scope) {
|
||||
if (!isset($table->$scope)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($data->$scope)) {
|
||||
$data->$scope = (object) [];
|
||||
}
|
||||
|
||||
if (!is_object($table->$scope)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldList = array_keys($this->metadata->get(['entityDefs', $scope, 'fields']) ?? []);
|
||||
|
||||
foreach (get_object_vars($table->$scope) as $field => $row) {
|
||||
if (!is_object($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!in_array($field, $fieldList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($data->$scope->$field)) {
|
||||
$data->$scope->$field = (object) [];
|
||||
}
|
||||
|
||||
foreach ($this->fieldActionList as $action) {
|
||||
if (!isset($row->$action)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$level = $row->$action;
|
||||
|
||||
if (!isset($data->$scope->$field->$action)) {
|
||||
$data->$scope->$field->$action = $level;
|
||||
} else {
|
||||
if (
|
||||
array_search(
|
||||
$data->$scope->$field->$action,
|
||||
$this->fieldLevelList
|
||||
) > array_search($level, $this->fieldLevelList)
|
||||
) {
|
||||
$data->$scope->$field->$action = $level;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
67
application/Espo/Core/Acl/Table/DefaultTableFactory.php
Normal file
67
application/Espo/Core/Acl/Table/DefaultTableFactory.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?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\Acl\Table;
|
||||
|
||||
use Espo\Entities\User;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Binding\Binder;
|
||||
use Espo\Core\Binding\BindingContainer;
|
||||
use Espo\Core\Binding\BindingData;
|
||||
use Espo\Core\InjectableFactory;
|
||||
|
||||
class DefaultTableFactory implements TableFactory
|
||||
{
|
||||
public function __construct(private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
/**
|
||||
* Create a table.
|
||||
*/
|
||||
public function create(User $user): Table
|
||||
{
|
||||
$bindingContainer = $this->createBindingContainer($user);
|
||||
|
||||
return $this->injectableFactory->createWithBinding(DefaultTable::class, $bindingContainer);
|
||||
}
|
||||
|
||||
private function createBindingContainer(User $user): BindingContainer
|
||||
{
|
||||
$bindingData = new BindingData();
|
||||
|
||||
$binder = new Binder($bindingData);
|
||||
|
||||
$binder
|
||||
->bindInstance(User::class, $user)
|
||||
->bindImplementation(RoleListProvider::class, DefaultRoleListProvider::class)
|
||||
->bindImplementation(CacheKeyProvider::class, DefaultCacheKeyProvider::class);
|
||||
|
||||
return new BindingContainer($bindingData);
|
||||
}
|
||||
}
|
||||
41
application/Espo/Core/Acl/Table/Role.php
Normal file
41
application/Espo/Core/Acl/Table/Role.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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\Acl\Table;
|
||||
|
||||
use stdClass;
|
||||
|
||||
interface Role
|
||||
{
|
||||
public function getScopeTableData(): stdClass;
|
||||
|
||||
public function getFieldTableData(): stdClass;
|
||||
|
||||
public function getPermissionLevel(string $permission): ?string;
|
||||
}
|
||||
55
application/Espo/Core/Acl/Table/RoleEntityWrapper.php
Normal file
55
application/Espo/Core/Acl/Table/RoleEntityWrapper.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\Acl\Table;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class RoleEntityWrapper implements Role
|
||||
{
|
||||
public function __construct(private Entity $entity)
|
||||
{}
|
||||
|
||||
public function getScopeTableData(): stdClass
|
||||
{
|
||||
return $this->entity->get('data') ?? (object) [];
|
||||
}
|
||||
|
||||
public function getFieldTableData(): stdClass
|
||||
{
|
||||
return $this->entity->get('fieldData') ?? (object) [];
|
||||
}
|
||||
|
||||
public function getPermissionLevel(string $permission): ?string
|
||||
{
|
||||
return $this->entity->get($permission . 'Permission');
|
||||
}
|
||||
}
|
||||
38
application/Espo/Core/Acl/Table/RoleListProvider.php
Normal file
38
application/Espo/Core/Acl/Table/RoleListProvider.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?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\Acl\Table;
|
||||
|
||||
interface RoleListProvider
|
||||
{
|
||||
/**
|
||||
* @return array<int, Role>
|
||||
*/
|
||||
public function get(): array;
|
||||
}
|
||||
66
application/Espo/Core/Acl/Table/ScopeDataResolver.php
Normal file
66
application/Espo/Core/Acl/Table/ScopeDataResolver.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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\Acl\Table;
|
||||
|
||||
use Espo\Core\Acl\ScopeData;
|
||||
use Espo\Core\Acl\Table;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ScopeDataResolver
|
||||
{
|
||||
public function __construct(
|
||||
private Table $table,
|
||||
) {}
|
||||
|
||||
public function resolve(mixed $data): ScopeData
|
||||
{
|
||||
if (!is_string($data)) {
|
||||
return ScopeData::fromRaw($data);
|
||||
}
|
||||
|
||||
$foreignScope = $data;
|
||||
$isBoolean = false;
|
||||
|
||||
if (str_starts_with($data, 'boolean:')) {
|
||||
[, $foreignScope] = explode(':', $data, 2);
|
||||
$isBoolean = true;
|
||||
}
|
||||
|
||||
$scopeData = $this->table->getScopeData($foreignScope);
|
||||
|
||||
if ($isBoolean && !$scopeData->isBoolean()) {
|
||||
return ScopeData::fromRaw(true);
|
||||
}
|
||||
|
||||
return $scopeData;
|
||||
}
|
||||
}
|
||||
35
application/Espo/Core/Acl/Table/ScopeDataType.php
Normal file
35
application/Espo/Core/Acl/Table/ScopeDataType.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\Acl\Table;
|
||||
|
||||
class ScopeDataType
|
||||
{
|
||||
const BOOLEAN = 'boolean';
|
||||
}
|
||||
42
application/Espo/Core/Acl/Table/TableFactory.php
Normal file
42
application/Espo/Core/Acl/Table/TableFactory.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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\Acl\Table;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\Core\Acl\Table;
|
||||
|
||||
interface TableFactory
|
||||
{
|
||||
/**
|
||||
* Create a table.
|
||||
*/
|
||||
public function create(User $user): Table;
|
||||
}
|
||||
@@ -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\Acl\Traits;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Core\Acl\DefaultAccessChecker;
|
||||
use Espo\Core\Acl\ScopeData;
|
||||
|
||||
trait DefaultAccessCheckerDependency
|
||||
{
|
||||
private DefaultAccessChecker $defaultAccessChecker;
|
||||
|
||||
public function check(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->defaultAccessChecker->check($user, $data);
|
||||
}
|
||||
|
||||
public function checkCreate(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->defaultAccessChecker->checkCreate($user, $data);
|
||||
}
|
||||
|
||||
public function checkRead(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->defaultAccessChecker->checkRead($user, $data);
|
||||
}
|
||||
|
||||
public function checkEdit(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->defaultAccessChecker->checkEdit($user, $data);
|
||||
}
|
||||
|
||||
public function checkDelete(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->defaultAccessChecker->checkDelete($user, $data);
|
||||
}
|
||||
|
||||
public function checkStream(User $user, ScopeData $data): bool
|
||||
{
|
||||
return $this->defaultAccessChecker->checkStream($user, $data);
|
||||
}
|
||||
|
||||
public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
return $this->defaultAccessChecker->checkEntityCreate($user, $entity, $data);
|
||||
}
|
||||
|
||||
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
return $this->defaultAccessChecker->checkEntityRead($user, $entity, $data);
|
||||
}
|
||||
|
||||
public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
return $this->defaultAccessChecker->checkEntityEdit($user, $entity, $data);
|
||||
}
|
||||
|
||||
public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
return $this->defaultAccessChecker->checkEntityDelete($user, $entity, $data);
|
||||
}
|
||||
|
||||
public function checkEntityStream(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
return $this->defaultAccessChecker->checkEntityStream($user, $entity, $data);
|
||||
}
|
||||
}
|
||||
770
application/Espo/Core/AclManager.php
Normal file
770
application/Espo/Core/AclManager.php
Normal file
@@ -0,0 +1,770 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\Acl\OwnershipSharedChecker;
|
||||
use Espo\Core\Acl\Permission;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Core\Acl\AccessChecker;
|
||||
use Espo\Core\Acl\AccessChecker\AccessCheckerFactory;
|
||||
use Espo\Core\Acl\AccessCreateChecker;
|
||||
use Espo\Core\Acl\AccessDeleteChecker;
|
||||
use Espo\Core\Acl\AccessEditChecker;
|
||||
use Espo\Core\Acl\AccessEntityCreateChecker;
|
||||
use Espo\Core\Acl\AccessEntityDeleteChecker;
|
||||
use Espo\Core\Acl\AccessEntityEditChecker;
|
||||
use Espo\Core\Acl\AccessEntityReadChecker;
|
||||
use Espo\Core\Acl\AccessEntityStreamChecker;
|
||||
use Espo\Core\Acl\AccessReadChecker;
|
||||
use Espo\Core\Acl\AccessStreamChecker;
|
||||
use Espo\Core\Acl\Exceptions\NotImplemented;
|
||||
use Espo\Core\Acl\GlobalRestriction;
|
||||
use Espo\Core\Acl\Map\Map;
|
||||
use Espo\Core\Acl\Map\MapFactory;
|
||||
use Espo\Core\Acl\OwnershipChecker;
|
||||
use Espo\Core\Acl\OwnershipChecker\OwnershipCheckerFactory;
|
||||
use Espo\Core\Acl\OwnershipOwnChecker;
|
||||
use Espo\Core\Acl\OwnershipTeamChecker;
|
||||
use Espo\Core\Acl\OwnerUserFieldProvider;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Acl\Table\TableFactory;
|
||||
|
||||
use stdClass;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* A central access point for access checking.
|
||||
*
|
||||
* @todo Refactor. Replace with an interface `Espo\Core\Acl\AclManager`.
|
||||
* Keep `Espo\Core\AclManager` as an extending interface for bc, bind it to the service.
|
||||
* Implementation in `Espo\Core\Acl\DefaultAclManager`.
|
||||
* The same for `Portal\AclManager`.
|
||||
*/
|
||||
class AclManager
|
||||
{
|
||||
protected const PERMISSION_ASSIGNMENT = Permission::ASSIGNMENT;
|
||||
|
||||
/** @var array<string, AccessChecker> */
|
||||
private $accessCheckerHashMap = [];
|
||||
/** @var array<string, OwnershipChecker> */
|
||||
private $ownershipCheckerHashMap = [];
|
||||
/** @var array<string, Table> */
|
||||
protected $tableHashMap = [];
|
||||
/** @var array<string, Map> */
|
||||
protected $mapHashMap = [];
|
||||
/** @var class-string */
|
||||
protected $userAclClassName = Acl::class;
|
||||
|
||||
/** @var array<string, class-string<AccessChecker>> */
|
||||
private $entityActionInterfaceMap = [
|
||||
Table::ACTION_CREATE => AccessEntityCreateChecker::class,
|
||||
Table::ACTION_READ => AccessEntityReadChecker::class,
|
||||
Table::ACTION_EDIT => AccessEntityEditChecker::class,
|
||||
Table::ACTION_DELETE => AccessEntityDeleteChecker::class,
|
||||
Table::ACTION_STREAM => AccessEntityStreamChecker::class,
|
||||
];
|
||||
/** @var array<string, class-string<AccessChecker>> */
|
||||
private $actionInterfaceMap = [
|
||||
Table::ACTION_CREATE => AccessCreateChecker::class,
|
||||
Table::ACTION_READ => AccessReadChecker::class,
|
||||
Table::ACTION_EDIT => AccessEditChecker::class,
|
||||
Table::ACTION_DELETE => AccessDeleteChecker::class,
|
||||
Table::ACTION_STREAM => AccessStreamChecker::class,
|
||||
];
|
||||
|
||||
/** @var AccessCheckerFactory|Portal\Acl\AccessChecker\AccessCheckerFactory */
|
||||
protected $accessCheckerFactory;
|
||||
/** @var OwnershipCheckerFactory|Portal\Acl\OwnershipChecker\OwnershipCheckerFactory */
|
||||
protected $ownershipCheckerFactory;
|
||||
|
||||
/** @var TableFactory */
|
||||
private $tableFactory;
|
||||
/** @var MapFactory */
|
||||
private $mapFactory;
|
||||
|
||||
public function __construct(
|
||||
AccessCheckerFactory $accessCheckerFactory,
|
||||
OwnershipCheckerFactory $ownershipCheckerFactory,
|
||||
TableFactory $tableFactory,
|
||||
MapFactory $mapFactory,
|
||||
protected GlobalRestriction $globalRestriction,
|
||||
protected OwnerUserFieldProvider $ownerUserFieldProvider,
|
||||
protected EntityManager $entityManager
|
||||
) {
|
||||
$this->accessCheckerFactory = $accessCheckerFactory;
|
||||
$this->ownershipCheckerFactory = $ownershipCheckerFactory;
|
||||
$this->tableFactory = $tableFactory;
|
||||
$this->mapFactory = $mapFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an access checker for a specific scope.
|
||||
*/
|
||||
protected function getAccessChecker(string $scope): AccessChecker
|
||||
{
|
||||
if (!array_key_exists($scope, $this->accessCheckerHashMap)) {
|
||||
$this->accessCheckerHashMap[$scope] = $this->accessCheckerFactory->create($scope, $this);
|
||||
}
|
||||
|
||||
return $this->accessCheckerHashMap[$scope];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ownership checker for a specific scope.
|
||||
*/
|
||||
protected function getOwnershipChecker(string $scope): OwnershipChecker
|
||||
{
|
||||
if (!array_key_exists($scope, $this->ownershipCheckerHashMap)) {
|
||||
$this->ownershipCheckerHashMap[$scope] = $this->ownershipCheckerFactory->create($scope, $this);
|
||||
}
|
||||
|
||||
return $this->ownershipCheckerHashMap[$scope];
|
||||
}
|
||||
|
||||
protected function getTable(User $user): Table
|
||||
{
|
||||
$key = $user->hasId() ? $user->getId() : spl_object_hash($user);
|
||||
|
||||
if (!array_key_exists($key, $this->tableHashMap)) {
|
||||
$this->tableHashMap[$key] = $this->tableFactory->create($user);
|
||||
}
|
||||
|
||||
return $this->tableHashMap[$key];
|
||||
}
|
||||
|
||||
protected function getMap(User $user): Map
|
||||
{
|
||||
$key = $user->hasId() ? $user->getId() : spl_object_hash($user);
|
||||
|
||||
if (!array_key_exists($key, $this->mapHashMap)) {
|
||||
$this->mapHashMap[$key] = $this->mapFactory->create($user, $this->getTable($user));
|
||||
}
|
||||
|
||||
return $this->mapHashMap[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a full access data map (for front-end).
|
||||
*/
|
||||
public function getMapData(User $user): stdClass
|
||||
{
|
||||
return $this->getMap($user)->getData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an access level for a specific scope and action.
|
||||
*
|
||||
* @param Table::ACTION_* $action
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function getLevel(User $user, string $scope, string $action): string
|
||||
{
|
||||
if (!$this->checkScope($user, $scope)) {
|
||||
return Table::LEVEL_NO;
|
||||
}
|
||||
|
||||
$data = $this->getTable($user)->getScopeData($scope);
|
||||
|
||||
return $data->get($action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a permission. E.g. 'assignment' permission.
|
||||
*/
|
||||
public function getPermissionLevel(User $user, string $permission): string
|
||||
{
|
||||
if (str_ends_with($permission, 'Permission')) {
|
||||
$permission = substr($permission, 0, -10);
|
||||
}
|
||||
|
||||
return $this->getTable($user)->getPermissionLevel($permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether there's no 'read' access for a specific scope.
|
||||
*/
|
||||
public function checkReadNo(User $user, string $scope): bool
|
||||
{
|
||||
return $this->getLevel($user, $scope, Table::ACTION_READ) === Table::LEVEL_NO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether 'read' access is set to 'team' for a specific scope.
|
||||
*/
|
||||
public function checkReadOnlyTeam(User $user, string $scope): bool
|
||||
{
|
||||
return $this->getLevel($user, $scope, Table::ACTION_READ) === Table::LEVEL_TEAM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether 'read' access is set to 'own' for a specific scope.
|
||||
*/
|
||||
public function checkReadOnlyOwn(User $user, string $scope): bool
|
||||
{
|
||||
return $this->getLevel($user, $scope, Table::ACTION_READ) === Table::LEVEL_OWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether 'read' access is set to 'all' for a specific scope.
|
||||
*/
|
||||
public function checkReadAll(User $user, string $scope): bool
|
||||
{
|
||||
return $this->getLevel($user, $scope, Table::ACTION_READ) === Table::LEVEL_ALL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a scope or entity. If $action is omitted, it will check whether
|
||||
* a scope level is set to 'enabled'.
|
||||
*
|
||||
* @param User $user A user to check for.
|
||||
* @param string|Entity $subject An entity type or entity.
|
||||
* @param Table::ACTION_*|null $action $action Action to check. Constants are available in the `Table` class.
|
||||
* @throws NotImplemented
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function check(User $user, $subject, ?string $action = null): bool
|
||||
{
|
||||
if (is_string($subject)) {
|
||||
return $this->checkScope($user, $subject, $action);
|
||||
}
|
||||
|
||||
/** @var mixed $entity */
|
||||
$entity = $subject;
|
||||
|
||||
if ($entity instanceof Entity) {
|
||||
$action = $action ?? Table::ACTION_READ;
|
||||
|
||||
return $this->checkEntity($user, $entity, $action);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException();
|
||||
}
|
||||
|
||||
/**
|
||||
* The same as `check` but does not throw NotImplemented exception.
|
||||
*
|
||||
* @param User $user A user to check for.
|
||||
* @param string|Entity $subject An entity type or entity.
|
||||
* @param Table::ACTION_*|null $action Action to check. Constants are available in the `Table` class.
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function tryCheck(User $user, $subject, ?string $action = null): bool
|
||||
{
|
||||
try {
|
||||
return $this->check($user, $subject, $action);
|
||||
} catch (NotImplemented) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access to a specific entity.
|
||||
*
|
||||
* @param User $user A user to check for.
|
||||
* @param Entity $entity An entity to check.
|
||||
* @param Table::ACTION_* $action Action to check. Constants are available in the `Table` class.
|
||||
* @throws NotImplemented
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function checkEntity(User $user, Entity $entity, string $action = Table::ACTION_READ): bool
|
||||
{
|
||||
$scope = $entity->getEntityType();
|
||||
|
||||
if (!$this->checkScope($user, $scope, $action)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $this->getTable($user)->getScopeData($scope);
|
||||
|
||||
$checker = $this->getAccessChecker($scope);
|
||||
|
||||
/** @var non-falsy-string $methodName */
|
||||
$methodName = 'checkEntity' . ucfirst($action);
|
||||
|
||||
$interface = $this->entityActionInterfaceMap[$action] ?? null;
|
||||
|
||||
if ($interface && $checker instanceof $interface && method_exists($checker, $methodName)) {
|
||||
return $checker->$methodName($user, $entity, $data);
|
||||
}
|
||||
|
||||
throw new NotImplemented("No entity access checker for '$scope' action '$action'.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 'read' access to a specific entity.
|
||||
*
|
||||
* @throws NotImplemented
|
||||
*/
|
||||
public function checkEntityRead(User $user, Entity $entity): bool
|
||||
{
|
||||
/** @noinspection PhpRedundantOptionalArgumentInspection */
|
||||
return $this->checkEntity($user, $entity, Table::ACTION_READ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 'create' access to a specific entity.
|
||||
*
|
||||
* @throws NotImplemented
|
||||
*/
|
||||
public function checkEntityCreate(User $user, Entity $entity): bool
|
||||
{
|
||||
return $this->checkEntity($user, $entity, Table::ACTION_CREATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 'edit' access to a specific entity.
|
||||
*
|
||||
* @throws NotImplemented
|
||||
*/
|
||||
public function checkEntityEdit(User $user, Entity $entity): bool
|
||||
{
|
||||
return $this->checkEntity($user, $entity, Table::ACTION_EDIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 'delete' access to a specific entity.
|
||||
*
|
||||
* @throws NotImplemented
|
||||
*/
|
||||
public function checkEntityDelete(User $user, Entity $entity): bool
|
||||
{
|
||||
return $this->checkEntity($user, $entity, Table::ACTION_DELETE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 'stream' access to a specific entity.
|
||||
*
|
||||
* @throws NotImplemented
|
||||
*/
|
||||
public function checkEntityStream(User $user, Entity $entity): bool
|
||||
{
|
||||
return $this->checkEntity($user, $entity, Table::ACTION_STREAM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a user is an owner of an entity.
|
||||
*/
|
||||
public function checkOwnershipOwn(User $user, Entity $entity): bool
|
||||
{
|
||||
$checker = $this->getOwnershipChecker($entity->getEntityType());
|
||||
|
||||
if (!$checker instanceof OwnershipOwnChecker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $checker->checkOwn($user, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an entity belongs to a user team.
|
||||
*/
|
||||
public function checkOwnershipTeam(User $user, Entity $entity): bool
|
||||
{
|
||||
$checker = $this->getOwnershipChecker($entity->getEntityType());
|
||||
|
||||
if (!$checker instanceof OwnershipTeamChecker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $checker->checkTeam($user, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an entity is shared with a user.
|
||||
*
|
||||
* @param Table::ACTION_* $action
|
||||
* @since 9.0.0
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function checkOwnershipShared(User $user, Entity $entity, string $action): bool
|
||||
{
|
||||
$checker = $this->getOwnershipChecker($entity->getEntityType());
|
||||
|
||||
if (!$checker instanceof OwnershipSharedChecker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $checker->checkShared($user, $entity, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access to scope. If $action is omitted, it will check whether a scope level is set to 'enabled'.
|
||||
*
|
||||
* @throws NotImplemented If not implemented by an access checker class.
|
||||
*/
|
||||
public function checkScope(User $user, string $scope, ?string $action = null): bool
|
||||
{
|
||||
if ($action && !$this->checkScope($user, $scope)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $this->getTable($user)->getScopeData($scope);
|
||||
|
||||
$checker = $this->getAccessChecker($scope);
|
||||
|
||||
if (!$action) {
|
||||
return $checker->check($user, $data);
|
||||
}
|
||||
|
||||
$methodName = 'check' . ucfirst($action);
|
||||
|
||||
$interface = $this->actionInterfaceMap[$action] ?? null;
|
||||
|
||||
if ($interface && $checker instanceof $interface) {
|
||||
return $checker->$methodName($user, $data);
|
||||
}
|
||||
|
||||
// For backward compatibility.
|
||||
$methodName = 'checkScope';
|
||||
|
||||
if (!method_exists($checker, $methodName)) {
|
||||
throw new NotImplemented("No access checker for '$scope' action '$action'.");
|
||||
}
|
||||
|
||||
return $checker->$methodName($user, $data, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,GlobalRestriction::TYPE_*>
|
||||
*/
|
||||
protected function getGlobalRestrictionTypeList(User $user, string $action = Table::ACTION_READ): array
|
||||
{
|
||||
$typeList = [
|
||||
GlobalRestriction::TYPE_FORBIDDEN,
|
||||
];
|
||||
|
||||
if ($action === Table::ACTION_READ) {
|
||||
$typeList[] = GlobalRestriction::TYPE_INTERNAL;
|
||||
}
|
||||
|
||||
if (!$user->isAdmin()) {
|
||||
$typeList[] = GlobalRestriction::TYPE_ONLY_ADMIN;
|
||||
}
|
||||
|
||||
if ($action === Table::ACTION_EDIT) {
|
||||
$typeList[] = GlobalRestriction::TYPE_READ_ONLY;
|
||||
|
||||
if (!$user->isAdmin()) {
|
||||
$typeList[] = GlobalRestriction::TYPE_NON_ADMIN_READ_ONLY;
|
||||
}
|
||||
}
|
||||
|
||||
return $typeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attributes forbidden for a user.
|
||||
*
|
||||
* @param Table::ACTION_READ|Table::ACTION_EDIT $action An action.
|
||||
* @param string $thresholdLevel Should not be used. Stands for possible future enhancements.
|
||||
* @return string[]
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function getScopeForbiddenAttributeList(
|
||||
User $user,
|
||||
string $scope,
|
||||
string $action = Table::ACTION_READ,
|
||||
string $thresholdLevel = Table::LEVEL_NO
|
||||
): array {
|
||||
|
||||
$list = array_merge(
|
||||
$this->getMap($user)->getScopeForbiddenAttributeList(
|
||||
$scope,
|
||||
$action,
|
||||
$thresholdLevel
|
||||
),
|
||||
$this->getScopeRestrictedAttributeList(
|
||||
$scope,
|
||||
$this->getGlobalRestrictionTypeList($user, $action)
|
||||
)
|
||||
);
|
||||
|
||||
return array_unique($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fields forbidden for a user.
|
||||
*
|
||||
* @param Table::ACTION_READ|Table::ACTION_EDIT $action An action.
|
||||
* @param string $thresholdLevel Should not be used. Stands for possible future enhancements.
|
||||
* @return string[]
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function getScopeForbiddenFieldList(
|
||||
User $user,
|
||||
string $scope,
|
||||
string $action = Table::ACTION_READ,
|
||||
string $thresholdLevel = Table::LEVEL_NO
|
||||
): array {
|
||||
|
||||
$list = array_merge(
|
||||
$this->getMap($user)->getScopeForbiddenFieldList(
|
||||
$scope,
|
||||
$action,
|
||||
$thresholdLevel
|
||||
),
|
||||
$this->getScopeRestrictedFieldList(
|
||||
$scope,
|
||||
$this->getGlobalRestrictionTypeList($user, $action)
|
||||
)
|
||||
);
|
||||
|
||||
return array_unique($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get links forbidden for a user.
|
||||
*
|
||||
* @param Table::ACTION_READ|Table::ACTION_EDIT $action An action.
|
||||
* @param string $thresholdLevel Should not be used. Stands for possible future enhancements.
|
||||
* @return string[]
|
||||
* @noinspection PhpUnusedParameterInspection
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function getScopeForbiddenLinkList(
|
||||
User $user,
|
||||
string $scope,
|
||||
string $action = Table::ACTION_READ,
|
||||
string $thresholdLevel = Table::LEVEL_NO
|
||||
): array {
|
||||
|
||||
return $this->getScopeRestrictedLinkList(
|
||||
$scope,
|
||||
$this->getGlobalRestrictionTypeList($user, $action)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access to a field.
|
||||
*
|
||||
* @param User $user A user.
|
||||
* @param string $scope A scope (entity type).
|
||||
* @param string $field A field to check.
|
||||
* @param Table::ACTION_READ|Table::ACTION_EDIT $action An action.
|
||||
* @return bool
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function checkField(User $user, string $scope, string $field, string $action = Table::ACTION_READ): bool
|
||||
{
|
||||
return !in_array($field, $this->getScopeForbiddenFieldList($user, $scope, $action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a user has access to another user over a specific permission.
|
||||
*
|
||||
* @param User|string $target User entity or user ID.
|
||||
*/
|
||||
public function checkUserPermission(User $user, $target, string $permissionType = Permission::USER): bool
|
||||
{
|
||||
$permission = $this->getPermissionLevel($user, $permissionType);
|
||||
|
||||
if (is_object($target)) {
|
||||
$userId = $target->getId();
|
||||
} else {
|
||||
$userId = $target;
|
||||
}
|
||||
|
||||
if ($user->getId() === $userId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($permission === Table::LEVEL_NO) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($permission === Table::LEVEL_YES) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($permission === Table::LEVEL_TEAM) {
|
||||
$teamIdList = $user->getLinkMultipleIdList(Field::TEAMS);
|
||||
|
||||
/** @var \Espo\Repositories\User $userRepository */
|
||||
$userRepository = $this->entityManager->getRepository(User::ENTITY_TYPE);
|
||||
|
||||
if (!$userRepository->checkBelongsToAnyOfTeams($userId, $teamIdList)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a user can assign to another user.
|
||||
*
|
||||
* @param User|string $target User entity or user ID.
|
||||
*/
|
||||
public function checkAssignmentPermission(User $user, $target): bool
|
||||
{
|
||||
return $this->checkUserPermission($user, $target, self::PERMISSION_ASSIGNMENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapper for a specific user.
|
||||
*/
|
||||
public function createUserAcl(User $user): Acl
|
||||
{
|
||||
$className = $this->userAclClassName;
|
||||
|
||||
/** @var Acl */
|
||||
return new $className($this, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a restricted field list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param GlobalRestriction::TYPE_*|array<int, GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedFieldList(string $scope, $type): array
|
||||
{
|
||||
$typeList = !is_array($type) ? [$type] : $type;
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach ($typeList as $type) {
|
||||
$list = array_merge(
|
||||
$list,
|
||||
$this->globalRestriction->getScopeRestrictedFieldList($scope, $type)
|
||||
);
|
||||
}
|
||||
|
||||
return array_unique($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a restricted attribute list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param GlobalRestriction::TYPE_*|array<int, GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedAttributeList(string $scope, $type): array
|
||||
{
|
||||
$typeList = !is_array($type) ? [$type] : $type;
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach ($typeList as $type) {
|
||||
$list = array_merge(
|
||||
$list,
|
||||
$this->globalRestriction->getScopeRestrictedAttributeList($scope, $type)
|
||||
);
|
||||
}
|
||||
|
||||
return array_unique($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a restricted link list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param GlobalRestriction::TYPE_*|array<int, GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedLinkList(string $scope, $type): array
|
||||
{
|
||||
$typeList = !is_array($type) ? [$type] : $type;
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach ($typeList as $type) {
|
||||
$list = array_merge(
|
||||
$list,
|
||||
$this->globalRestriction->getScopeRestrictedLinkList($scope, $type)
|
||||
);
|
||||
}
|
||||
|
||||
return array_unique($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity field that stores an owner-user (or multiple users).
|
||||
* Must be a link or linkMultiple field. NULL means no owner.
|
||||
*/
|
||||
public function getReadOwnerUserField(string $entityType): ?string
|
||||
{
|
||||
return $this->ownerUserFieldProvider->get($entityType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `getPermissionLevel` instead.
|
||||
*/
|
||||
public function get(User $user, string $permission): string
|
||||
{
|
||||
return $this->getPermissionLevel($user, $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `checkUserPermission` instead.
|
||||
*/
|
||||
public function checkUser(User $user, string $permission, User $target): bool
|
||||
{
|
||||
if ($this->getPermissionLevel($user, $permission) === Table::LEVEL_ALL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->getPermissionLevel($user, $permission) === Table::LEVEL_NO) {
|
||||
if ($target->getId() === $user->getId()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->getPermissionLevel($user, $permission) === Table::LEVEL_TEAM) {
|
||||
if ($target->getId() === $user->getId()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$targetTeamIdList = $target->getTeamIdList();
|
||||
|
||||
$inTeam = false;
|
||||
|
||||
foreach ($user->getTeamIdList() as $id) {
|
||||
if (in_array($id, $targetTeamIdList)) {
|
||||
$inTeam = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($inTeam) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
44
application/Espo/Core/Action/Action.php
Normal file
44
application/Espo/Core/Action/Action.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?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\Action;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
interface Action
|
||||
{
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function process(Params $params, Data $data): void;
|
||||
}
|
||||
149
application/Espo/Core/Action/ActionFactory.php
Normal file
149
application/Espo/Core/Action/ActionFactory.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?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\Action;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
class ActionFactory
|
||||
{
|
||||
public function __construct(private Metadata $metadata, private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function create(string $action, ?string $entityType = null): Action
|
||||
{
|
||||
$className = $this->getClassName($action, $entityType);
|
||||
|
||||
if (!$className) {
|
||||
throw new NotFound("Action '{$action}' not found.");
|
||||
}
|
||||
|
||||
if ($entityType && $this->isDisabled($action, $entityType)) {
|
||||
throw new Forbidden("Action '{$action}' is disabled for '{$entityType}'.");
|
||||
}
|
||||
|
||||
return $this->injectableFactory->create($className);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,object> $with
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @todo Remove.
|
||||
* @deprecated
|
||||
*/
|
||||
public function createWith(string $action, ?string $entityType, array $with): Action
|
||||
{
|
||||
$className = $this->getClassName($action, $entityType);
|
||||
|
||||
if (!$className) {
|
||||
throw new NotFound("Action '{$action}' not found.");
|
||||
}
|
||||
|
||||
if ($entityType && $this->isDisabled($action, $entityType)) {
|
||||
throw new Forbidden("Action '{$action}' is disabled for '{$entityType}'.");
|
||||
}
|
||||
|
||||
return $this->injectableFactory->createWith($className, $with);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?class-string<Action>
|
||||
*/
|
||||
private function getClassName(string $action, ?string $entityType): ?string
|
||||
{
|
||||
if ($entityType) {
|
||||
$className = $this->getEntityTypeClassName($action, $entityType);
|
||||
|
||||
if ($className) {
|
||||
return $className;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var ?class-string<Action> */
|
||||
return $this->metadata->get(
|
||||
['app', 'actions', $action, 'implementationClassName']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?class-string<Action>
|
||||
*/
|
||||
private function getEntityTypeClassName(string $action, string $entityType): ?string
|
||||
{
|
||||
/** @var ?class-string<Action> */
|
||||
return $this->metadata->get(
|
||||
['recordDefs', $entityType, 'actions', $action, 'implementationClassName']
|
||||
);
|
||||
}
|
||||
|
||||
private function isDisabled(string $action, string $entityType): bool
|
||||
{
|
||||
$actionsDisabled = $this->metadata
|
||||
->get(['recordDefs', $entityType, 'actionsDisabled']) ?? false;
|
||||
|
||||
if ($actionsDisabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->needsToBeAllowed($entityType)) {
|
||||
if (!$this->isAllowed($action, $entityType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->metadata
|
||||
->get(['recordDefs', $entityType, 'actions', $action, 'disabled']) ?? false;
|
||||
}
|
||||
|
||||
private function needsToBeAllowed(string $entityType): bool
|
||||
{
|
||||
$isObject = $this->metadata->get(['scopes', $entityType, 'object']) ?? false;
|
||||
|
||||
if (!$isObject) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->metadata
|
||||
->get(['recordDefs', $entityType, 'notAllowedActionsDisabled']) ?? false;
|
||||
}
|
||||
|
||||
private function isAllowed(string $action, string $entityType): bool
|
||||
{
|
||||
return $this->metadata
|
||||
->get(['recordDefs', $entityType, 'actions', $action, 'allowed']) ?? false;
|
||||
}
|
||||
}
|
||||
142
application/Espo/Core/Action/Actions/ConvertCurrency.php
Normal file
142
application/Espo/Core/Action/Actions/ConvertCurrency.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?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\Action\Actions;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Action\Action;
|
||||
use Espo\Core\Action\Data;
|
||||
use Espo\Core\Action\Params;
|
||||
use Espo\Core\Currency\ConfigDataProvider as CurrencyConfigDataProvider;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Currency\Rates as CurrencyRates;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Core\ORM\Repository\Option\SaveOption;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Tools\Currency\Conversion\EntityConverterFactory;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class ConvertCurrency implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private EntityConverterFactory $converterFactory,
|
||||
private Acl $acl,
|
||||
private EntityManager $entityManager,
|
||||
private Metadata $metadata,
|
||||
private CurrencyConfigDataProvider $configDataProvider,
|
||||
private User $user
|
||||
) {}
|
||||
|
||||
public function process(Params $params, Data $data): void
|
||||
{
|
||||
$entityType = $params->getEntityType();
|
||||
$id = $params->getId();
|
||||
|
||||
if (!$this->acl->checkScope($entityType, Acl\Table::ACTION_EDIT)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$this->checkFieldAccess($entityType);
|
||||
|
||||
$baseCurrency = $this->configDataProvider->getBaseCurrency();
|
||||
$targetCurrency = $data->get('targetCurrency');
|
||||
|
||||
if (!$targetCurrency) {
|
||||
throw new BadRequest("No target currency.");
|
||||
}
|
||||
|
||||
$rates = $this->getRatesFromData($data) ??
|
||||
$this->configDataProvider->getCurrencyRates();
|
||||
|
||||
if ($targetCurrency !== $baseCurrency && !$rates->hasRate($targetCurrency)) {
|
||||
throw new BadRequest("Target currency rate is not specified.");
|
||||
}
|
||||
|
||||
$entity = $this->entityManager->getEntityById($entityType, $id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$this->acl->checkEntityEdit($entity)) {
|
||||
throw new Forbidden("No 'edit' access.");
|
||||
}
|
||||
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
throw new RuntimeException("Only Core-Entity allowed.");
|
||||
}
|
||||
|
||||
$converter = $this->converterFactory->create($entityType);
|
||||
|
||||
$converter->convert($entity, $targetCurrency, $rates);
|
||||
|
||||
$this->entityManager->saveEntity($entity, [SaveOption::MODIFIED_BY_ID => $this->user->getId()]);
|
||||
}
|
||||
|
||||
private function getRatesFromData(Data $data): ?CurrencyRates
|
||||
{
|
||||
if ($data->get('rates') === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$baseCurrency = $this->configDataProvider->getBaseCurrency();
|
||||
|
||||
$ratesArray = get_object_vars($data->get('rates'));
|
||||
$ratesArray[$baseCurrency] = 1.0;
|
||||
|
||||
return CurrencyRates::fromAssoc($ratesArray, $baseCurrency);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function checkFieldAccess(string $entityType): void
|
||||
{
|
||||
/** @var string[] $requiredFieldList */
|
||||
$requiredFieldList = $this->metadata->get(['scopes', $entityType, 'currencyConversionAccessRequiredFieldList']);
|
||||
|
||||
if ($requiredFieldList === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($requiredFieldList as $field) {
|
||||
if (!$this->acl->checkField($entityType, $field, Table::ACTION_EDIT)) {
|
||||
throw new Forbidden("No edit access to field `$field`.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
application/Espo/Core/Action/Actions/Merge.php
Normal file
69
application/Espo/Core/Action/Actions/Merge.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?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\Action\Actions;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Action\Action;
|
||||
use Espo\Core\Action\Actions\Merge\Merger;
|
||||
use Espo\Core\Action\Data;
|
||||
use Espo\Core\Action\Params;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class Merge implements Action
|
||||
{
|
||||
public function __construct(private Acl $acl, private Merger $merger)
|
||||
{}
|
||||
|
||||
public function process(Params $params, Data $data): void
|
||||
{
|
||||
$entityType = $params->getEntityType();
|
||||
|
||||
if (!$this->acl->checkScope($entityType, Table::ACTION_EDIT)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$sourceIdList = $data->get('sourceIdList');
|
||||
$attributes = $data->get('attributes');
|
||||
|
||||
if (!is_array($sourceIdList)) {
|
||||
throw new BadRequest("No 'sourceIdList'.");
|
||||
}
|
||||
|
||||
if (!$attributes instanceof stdClass) {
|
||||
throw new BadRequest("No 'attributes'.");
|
||||
}
|
||||
|
||||
$this->merger->process($params, $sourceIdList, $attributes);
|
||||
}
|
||||
}
|
||||
431
application/Espo/Core/Action/Actions/Merge/Merger.php
Normal file
431
application/Espo/Core/Action/Actions/Merge/Merger.php
Normal file
@@ -0,0 +1,431 @@
|
||||
<?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\Action\Actions\Merge;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Action\Params;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Name\Link;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Record\ActionHistory\Action;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\ObjectUtil;
|
||||
use Espo\Entities\Note;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\EmailAddress;
|
||||
use Espo\Entities\PhoneNumber;
|
||||
|
||||
use Espo\ORM\Type\RelationType;
|
||||
use stdClass;
|
||||
|
||||
class Merger
|
||||
{
|
||||
public function __construct(
|
||||
private Acl $acl,
|
||||
private Metadata $metadata,
|
||||
private EntityManager $entityManager,
|
||||
private ServiceContainer $serviceContainer
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param string[] $sourceIdList
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function process(Params $params, array $sourceIdList, stdClass $data): void
|
||||
{
|
||||
$clonedData = ObjectUtil::clone($data);
|
||||
|
||||
$entityType = $params->getEntityType();
|
||||
$id = $params->getId();
|
||||
|
||||
$entity = $this->entityManager->getEntityById($entityType, $id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new NotFound("Record not found.");
|
||||
}
|
||||
|
||||
if (!$this->acl->check($entity, Table::ACTION_EDIT)) {
|
||||
throw new Forbidden("No edit access.");
|
||||
}
|
||||
|
||||
$service = $this->serviceContainer->get($entityType);
|
||||
|
||||
$service->filterUpdateInput($clonedData);
|
||||
|
||||
$entity->set($clonedData);
|
||||
|
||||
$this->unsetNotActualAttributes($entity);
|
||||
|
||||
if (!$service->checkAssignment($entity)) {
|
||||
throw new Forbidden("Assignment permission failure.");
|
||||
}
|
||||
|
||||
$sourceEntityList = $this->fetchSourceEntityList($entityType, $sourceIdList);
|
||||
|
||||
$entityDefs = $this->entityManager->getDefs()->getEntity($entityType);
|
||||
|
||||
$hasPhoneNumber =
|
||||
$entityDefs->hasField(Field::PHONE_NUMBER) &&
|
||||
$entityDefs->getField(Field::PHONE_NUMBER)->getType() === FieldType::PHONE;
|
||||
|
||||
$hasEmailAddress =
|
||||
$entityDefs->hasField(Field::EMAIL_ADDRESS) &&
|
||||
$entityDefs->getField(Field::EMAIL_ADDRESS)->getType() === FieldType::EMAIL;
|
||||
|
||||
if ($hasPhoneNumber) {
|
||||
$phoneNumberToRelateList = $this->fetchEntityPhoneNumberList($entity);
|
||||
}
|
||||
|
||||
if ($hasEmailAddress) {
|
||||
$emailAddressToRelateList = $this->fetchEntityEmailAddressList($entity);
|
||||
}
|
||||
|
||||
foreach ($sourceEntityList as $sourceEntity) {
|
||||
if ($hasPhoneNumber) {
|
||||
$phoneNumberToRelateList = array_merge(
|
||||
$phoneNumberToRelateList,
|
||||
$this->fetchEntityPhoneNumberList($sourceEntity)
|
||||
);
|
||||
}
|
||||
|
||||
if ($hasEmailAddress) {
|
||||
$emailAddressToRelateList = array_merge(
|
||||
$emailAddressToRelateList,
|
||||
$this->fetchEntityEmailAddressList($sourceEntity)
|
||||
);
|
||||
}
|
||||
|
||||
$this->updateNotes($sourceEntity, $entity);
|
||||
}
|
||||
|
||||
$mergeLinkList = $this->getMergeLinkList($entityType);
|
||||
|
||||
foreach ($sourceEntityList as $sourceEntity) {
|
||||
foreach ($mergeLinkList as $link) {
|
||||
$this->updateRelations($sourceEntity, $entity, $link);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($sourceEntityList as $sourceEntity) {
|
||||
$this->entityManager->removeEntity($sourceEntity);
|
||||
|
||||
$service->processActionHistoryRecord(Action::DELETE, $sourceEntity);
|
||||
}
|
||||
|
||||
if ($hasPhoneNumber) {
|
||||
$this->preparePhoneNumberData($phoneNumberToRelateList, $clonedData);
|
||||
}
|
||||
|
||||
if ($hasEmailAddress) {
|
||||
$this->prepareEmailAddressData($emailAddressToRelateList, $clonedData);
|
||||
}
|
||||
|
||||
$entity->set($clonedData);
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
|
||||
$service->processActionHistoryRecord(Action::UPDATE, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $sourceIdList
|
||||
* @return Entity[]
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function fetchSourceEntityList(string $entityType, array $sourceIdList): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
foreach ($sourceIdList as $sourceId) {
|
||||
$sourceEntity = $this->entityManager->getEntityById($entityType, $sourceId);
|
||||
|
||||
if (!$sourceEntity) {
|
||||
throw new NotFound("Source record not found.");
|
||||
}
|
||||
|
||||
$list[] = $sourceEntity;
|
||||
|
||||
if (
|
||||
!$this->acl->check($sourceEntity, Table::ACTION_READ) ||
|
||||
!$this->acl->check($sourceEntity, Table::ACTION_EDIT) ||
|
||||
!$this->acl->check($sourceEntity, Table::ACTION_DELETE)
|
||||
) {
|
||||
throw new Forbidden("No read, edit or delete access for one of source entities.");
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PhoneNumber[]
|
||||
*/
|
||||
private function fetchEntityPhoneNumberList(Entity $entity): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
/** @var iterable<PhoneNumber> $collection */
|
||||
$collection = $this->entityManager
|
||||
->getRelation($entity, 'phoneNumbers')
|
||||
->find();
|
||||
|
||||
foreach ($collection as $entity) {
|
||||
$list[] = $entity;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailAddress[]
|
||||
*/
|
||||
private function fetchEntityEmailAddressList(Entity $entity): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
/** @var iterable<EmailAddress> $collection */
|
||||
$collection = $this->entityManager
|
||||
->getRelation($entity, Link::EMAIL_ADDRESSES)
|
||||
->find();
|
||||
|
||||
foreach ($collection as $entity) {
|
||||
$list[] = $entity;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
private function updateNotes(Entity $sourceEntity, Entity $targetEntity): void
|
||||
{
|
||||
$updateQuery = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->update()
|
||||
->in(Note::ENTITY_TYPE)
|
||||
->set([
|
||||
'parentId' => $targetEntity->getId(),
|
||||
'parentType' => $targetEntity->getEntityType(),
|
||||
])
|
||||
->where([
|
||||
'type' => [
|
||||
Note::TYPE_POST,
|
||||
Note::TYPE_EMAIL_SENT,
|
||||
Note::TYPE_EMAIL_RECEIVED,
|
||||
],
|
||||
'parentId' => $sourceEntity->getId(),
|
||||
'parentType' => $sourceEntity->getEntityType(),
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager
|
||||
->getQueryExecutor()
|
||||
->execute($updateQuery);
|
||||
}
|
||||
|
||||
private function updateRelations(Entity $sourceEntity, Entity $targetEntity, string $link): void
|
||||
{
|
||||
$entityType = $sourceEntity->getEntityType();
|
||||
|
||||
$columnAttributeMap = $this->getLinkColumnAttributeMap($entityType, $link);
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRelation($sourceEntity, $link)
|
||||
->find();
|
||||
|
||||
foreach ($collection as $relatedEntity) {
|
||||
$map = null;
|
||||
|
||||
if ($columnAttributeMap) {
|
||||
$map = array_map(fn ($attribute) => $relatedEntity->get($attribute), $columnAttributeMap);
|
||||
}
|
||||
|
||||
$this->entityManager
|
||||
->getRelation($targetEntity, $link)
|
||||
->relate($relatedEntity, $map);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getMergeLinkList(string $entityType): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
$entityDefs = $this->entityManager->getDefs()->getEntity($entityType);
|
||||
|
||||
$ignoreList = [
|
||||
Link::EMAIL_ADDRESSES,
|
||||
'phoneNumbers',
|
||||
];
|
||||
|
||||
foreach ($entityDefs->getRelationList() as $relationDefs) {
|
||||
$name = $relationDefs->getName();
|
||||
$type = $relationDefs->getType();
|
||||
|
||||
if (in_array($name, $ignoreList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$notMergeable = $this->metadata
|
||||
->get(['entityDefs', $entityType, 'links', $name, 'notMergeable']);
|
||||
|
||||
if ($notMergeable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
$type !== Entity::HAS_MANY &&
|
||||
$type !== Entity::HAS_CHILDREN &&
|
||||
$type !== Entity::MANY_MANY
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[] = $name;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhoneNumber[] $phoneNumberList
|
||||
*/
|
||||
private function preparePhoneNumberData(array $phoneNumberList, stdClass $data): void
|
||||
{
|
||||
$phoneNumberData = [];
|
||||
|
||||
foreach ($phoneNumberList as $i => $phoneNumber) {
|
||||
$o = (object) [];
|
||||
|
||||
$o->phoneNumber = $phoneNumber->getNumber();
|
||||
$o->primary = false;
|
||||
|
||||
if (empty($data->phoneNumber) && $i === 0) {
|
||||
$o->primary = true;
|
||||
}
|
||||
|
||||
if (!empty($data->phoneNumber)) {
|
||||
$o->primary = $o->phoneNumber === $data->phoneNumber;
|
||||
}
|
||||
|
||||
$o->optOut = $phoneNumber->isOptedOut();
|
||||
$o->invalid = $phoneNumber->isInvalid();
|
||||
$o->type = $phoneNumber->getType();
|
||||
|
||||
$phoneNumberData[] = $o;
|
||||
}
|
||||
|
||||
$data->phoneNumberData = $phoneNumberData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EmailAddress[] $emailAddressList
|
||||
*/
|
||||
private function prepareEmailAddressData(array $emailAddressList, stdClass $data): void
|
||||
{
|
||||
$emailAddressData = [];
|
||||
|
||||
foreach ($emailAddressList as $i => $emailAddress) {
|
||||
$o = (object) [];
|
||||
|
||||
$o->emailAddress = $emailAddress->getAddress();
|
||||
$o->primary = false;
|
||||
|
||||
if (empty($data->emailAddress) && $i === 0) {
|
||||
$o->primary = true;
|
||||
}
|
||||
|
||||
if (!empty($data->emailAddress)) {
|
||||
$o->primary = $o->emailAddress === $data->emailAddress;
|
||||
}
|
||||
|
||||
$o->optOut = $emailAddress->isOptedOut();
|
||||
$o->invalid = $emailAddress->isInvalid();
|
||||
|
||||
$emailAddressData[] = $o;
|
||||
}
|
||||
|
||||
$data->emailAddressData = $emailAddressData;
|
||||
}
|
||||
|
||||
private function unsetNotActualAttributes(Entity $entity): void
|
||||
{
|
||||
$fieldDefsList = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entity->getEntityType())
|
||||
->getFieldList();
|
||||
|
||||
foreach ($fieldDefsList as $fieldDefs) {
|
||||
$field = $fieldDefs->getName();
|
||||
|
||||
if ($fieldDefs->getType() === FieldType::LINK && $entity->isAttributeChanged($field . 'Id')) {
|
||||
$entity->clear($field . 'Name');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?array<string, string>
|
||||
*/
|
||||
private function getLinkColumnAttributeMap(string $entityType, string $link): ?array
|
||||
{
|
||||
$entityDefs = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType);
|
||||
|
||||
$columnAttributeMap = null;
|
||||
|
||||
$relationDefs = $entityDefs->tryGetRelation($link);
|
||||
|
||||
if (
|
||||
$relationDefs &&
|
||||
$relationDefs->getType() === RelationType::MANY_MANY &&
|
||||
$relationDefs->hasForeignEntityType() &&
|
||||
$relationDefs->hasForeignRelationName()
|
||||
) {
|
||||
$foreignRelationDefs = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($relationDefs->getForeignEntityType())
|
||||
->getRelation($relationDefs->getForeignRelationName());
|
||||
|
||||
/** ?@var array<string, string> $columnAttributeMap */
|
||||
$columnAttributeMap = $foreignRelationDefs->getParam('columnAttributeMap');
|
||||
}
|
||||
|
||||
return $columnAttributeMap;
|
||||
}
|
||||
}
|
||||
64
application/Espo/Core/Action/Api/PostProcess.php
Normal file
64
application/Espo/Core/Action/Api/PostProcess.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?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\Action\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Action\Service;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
/**
|
||||
* Processes actions.
|
||||
*/
|
||||
class PostProcess implements Action
|
||||
{
|
||||
public function __construct(private Service $service)
|
||||
{}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$body = $request->getParsedBody();
|
||||
|
||||
$entityType = $body->entityType ?? null;
|
||||
$id = $body->id ?? null;
|
||||
$action = $body->action ?? null;
|
||||
$data = $body->data ?? (object) [];
|
||||
|
||||
if (!$entityType || !$action || !$id) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$entity = $this->service->process($entityType, $action, $id, $data);
|
||||
|
||||
return ResponseComposer::json($entity->getValueMap());
|
||||
}
|
||||
}
|
||||
95
application/Espo/Core/Action/Data.php
Normal file
95
application/Espo/Core/Action/Data.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\Action;
|
||||
|
||||
use Espo\Core\Utils\ObjectUtil;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class Data
|
||||
{
|
||||
private stdClass $data;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
$this->data = (object) [];
|
||||
}
|
||||
|
||||
public function getRaw(): stdClass
|
||||
{
|
||||
return ObjectUtil::clone($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item value.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $name)
|
||||
{
|
||||
return $this->getRaw()->$name ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has an item.
|
||||
*/
|
||||
public function has(string $name): bool
|
||||
{
|
||||
return property_exists($this->data, $name);
|
||||
}
|
||||
|
||||
public static function fromRaw(stdClass $data): self
|
||||
{
|
||||
$obj = new self();
|
||||
|
||||
$obj->data = $data;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone with an item value.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function with(string $name, $value): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
|
||||
$obj->data->$name = $value;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->data = ObjectUtil::clone($this->data);
|
||||
}
|
||||
}
|
||||
56
application/Espo/Core/Action/Params.php
Normal file
56
application/Espo/Core/Action/Params.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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\Action;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Immutable.
|
||||
*/
|
||||
class Params
|
||||
{
|
||||
public function __construct(private string $entityType, private string $id)
|
||||
{
|
||||
|
||||
if (!$entityType || !$id) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
public function getEntityType(): string
|
||||
{
|
||||
return $this->entityType;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
82
application/Espo/Core/Action/Service.php
Normal file
82
application/Espo/Core/Action/Service.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?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\Action;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Record\ReadParams;
|
||||
use Espo\Core\Record\ServiceContainer as RecordServiceContainer;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class Service
|
||||
{
|
||||
public function __construct(
|
||||
private ActionFactory $factory,
|
||||
private Acl $acl,
|
||||
private RecordServiceContainer $recordServiceContainer
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Perform an action.
|
||||
*
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function process(string $entityType, string $action, string $id, stdClass $data): Entity
|
||||
{
|
||||
if (!$this->acl->checkScope($entityType)) {
|
||||
throw new ForbiddenSilent();
|
||||
}
|
||||
|
||||
if (!$action || !$id) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$actionParams = new Params($entityType, $id);
|
||||
|
||||
$actionProcessor = $this->factory->create($action, $entityType);
|
||||
|
||||
$actionProcessor->process(
|
||||
$actionParams,
|
||||
Data::fromRaw($data)
|
||||
);
|
||||
|
||||
$service = $this->recordServiceContainer->get($entityType);
|
||||
|
||||
return $service->read($id, ReadParams::create());
|
||||
}
|
||||
}
|
||||
55
application/Espo/Core/Api/Action.php
Normal file
55
application/Espo/Core/Api/Action.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\Api;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
/**
|
||||
* A route action.
|
||||
*/
|
||||
interface Action
|
||||
{
|
||||
/**
|
||||
* Process.
|
||||
*
|
||||
* @param Request $request A request.
|
||||
* @return Response A response. Use ResponseComposer for building.
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Conflict
|
||||
* @throws Error
|
||||
*/
|
||||
public function process(Request $request): Response;
|
||||
}
|
||||
122
application/Espo/Core/Api/ActionHandler.php
Normal file
122
application/Espo/Core/Api/ActionHandler.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?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\Api;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
use Espo\Core\Utils\Config;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Psr7Response;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Slim\Psr7\Factory\ResponseFactory;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ActionHandler implements RequestHandlerInterface
|
||||
{
|
||||
private const DEFAULT_CONTENT_TYPE = 'application/json';
|
||||
|
||||
public function __construct(
|
||||
private Action $action,
|
||||
private ProcessData $processData,
|
||||
private Config $config
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$requestWrapped = new RequestWrapper(
|
||||
$request,
|
||||
$this->processData->getBasePath(),
|
||||
$this->processData->getRouteParams()
|
||||
);
|
||||
|
||||
$response = $this->action->process($requestWrapped);
|
||||
|
||||
return $this->prepareResponse($response);
|
||||
}
|
||||
|
||||
private function prepareResponse(Response $response): Psr7Response
|
||||
{
|
||||
if (!$response->hasHeader('Content-Type')) {
|
||||
$response->setHeader('Content-Type', self::DEFAULT_CONTENT_TYPE);
|
||||
}
|
||||
|
||||
if (!$response->hasHeader('Cache-Control')) {
|
||||
$response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
|
||||
}
|
||||
|
||||
if (!$response->hasHeader('Expires')) {
|
||||
$response->setHeader('Expires', '0');
|
||||
}
|
||||
|
||||
if (!$response->hasHeader('Last-Modified')) {
|
||||
$response->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
|
||||
}
|
||||
|
||||
$response->setHeader('X-App-Timestamp', (string) ($this->config->get('appTimestamp') ?? '0'));
|
||||
|
||||
/** @noinspection PhpConditionAlreadyCheckedInspection */
|
||||
return $response instanceof ResponseWrapper ?
|
||||
$response->toPsr7() :
|
||||
self::responseToPsr7($response);
|
||||
}
|
||||
|
||||
private static function responseToPsr7(Response $response): Psr7Response
|
||||
{
|
||||
$psr7Response = (new ResponseFactory())->createResponse();
|
||||
|
||||
$statusCode = $response->getStatusCode();
|
||||
$reason = $response->getReasonPhrase();
|
||||
$body = $response->getBody();
|
||||
|
||||
$psr7Response = $psr7Response
|
||||
->withStatus($statusCode, $reason)
|
||||
->withBody($body);
|
||||
|
||||
foreach ($response->getHeaderNames() as $name) {
|
||||
$psr7Response = $psr7Response->withHeader($name, $response->getHeaderAsArray($name));
|
||||
}
|
||||
|
||||
return $psr7Response;
|
||||
}
|
||||
}
|
||||
347
application/Espo/Core/Api/Auth.php
Normal file
347
application/Espo/Core/Api/Auth.php
Normal file
@@ -0,0 +1,347 @@
|
||||
<?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\Api;
|
||||
|
||||
use Espo\Core\Authentication\HeaderKey;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\ServiceUnavailable;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Authentication\ConfigDataProvider;
|
||||
use Espo\Core\Authentication\Authentication;
|
||||
use Espo\Core\Authentication\AuthenticationData;
|
||||
use Espo\Core\Authentication\Result;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Utils\Json;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Determines which auth method to use. Fetches a username and password from headers and server parameters.
|
||||
* Then tries to log in.
|
||||
*/
|
||||
class Auth
|
||||
{
|
||||
public function __construct(
|
||||
private Log $log,
|
||||
private Authentication $authentication,
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
private bool $authRequired = true,
|
||||
private bool $isEntryPoint = false
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Exception
|
||||
*/
|
||||
public function process(Request $request, Response $response): AuthResult
|
||||
{
|
||||
$username = null;
|
||||
$password = null;
|
||||
|
||||
$authenticationMethod = $this->obtainAuthenticationMethodFromRequest($request);
|
||||
|
||||
if (!$authenticationMethod) {
|
||||
[$username, $password] = $this->obtainUsernamePasswordFromRequest($request);
|
||||
}
|
||||
|
||||
$authenticationData = AuthenticationData::create()
|
||||
->withUsername($username)
|
||||
->withPassword($password)
|
||||
->withMethod($authenticationMethod);
|
||||
|
||||
$hasAuthData = $username || $authenticationMethod;
|
||||
|
||||
if (!$hasAuthData) {
|
||||
$password = $this->obtainTokenFromCookies($request);
|
||||
|
||||
if ($password) {
|
||||
$authenticationData = AuthenticationData::create()
|
||||
->withPassword($password)
|
||||
->withByTokenOnly(true);
|
||||
|
||||
$hasAuthData = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->authRequired && !$this->isEntryPoint && $hasAuthData) {
|
||||
$authResult = $this->processAuthNotRequired(
|
||||
$authenticationData,
|
||||
$request,
|
||||
$response
|
||||
);
|
||||
|
||||
if ($authResult) {
|
||||
return $authResult;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->authRequired) {
|
||||
return AuthResult::createResolvedUseNoAuth();
|
||||
}
|
||||
|
||||
if ($hasAuthData) {
|
||||
return $this->processWithAuthData($authenticationData, $request, $response);
|
||||
}
|
||||
|
||||
$showDialog =
|
||||
($this->isEntryPoint || !$this->isXMLHttpRequest($request)) &&
|
||||
!$request->getHeader('Referer') &&
|
||||
$request->getMethod() !== Method::POST;
|
||||
|
||||
$this->handleUnauthorized($response, null, $showDialog);
|
||||
|
||||
return AuthResult::createNotResolved();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function processAuthNotRequired(
|
||||
AuthenticationData $data,
|
||||
Request $request,
|
||||
Response $response
|
||||
): ?AuthResult {
|
||||
|
||||
try {
|
||||
$result = $this->authentication->login($data, $request, $response);
|
||||
} catch (Exception $e) {
|
||||
$this->handleException($response, $e);
|
||||
|
||||
return AuthResult::createNotResolved();
|
||||
}
|
||||
|
||||
if (!$result->isFail()) {
|
||||
return AuthResult::createResolved();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function processWithAuthData(
|
||||
AuthenticationData $data,
|
||||
Request $request,
|
||||
Response $response
|
||||
): AuthResult {
|
||||
|
||||
try {
|
||||
$result = $this->authentication->login($data, $request, $response);
|
||||
} catch (Exception $e) {
|
||||
$this->handleException($response, $e);
|
||||
|
||||
return AuthResult::createNotResolved();
|
||||
}
|
||||
|
||||
if ($result->isSuccess()) {
|
||||
return AuthResult::createResolved();
|
||||
}
|
||||
|
||||
if ($result->isFail()) {
|
||||
$showDialog =
|
||||
$this->isEntryPoint &&
|
||||
!$request->getHeader('Referer') &&
|
||||
$request->getMethod() !== Method::POST;
|
||||
|
||||
$this->handleUnauthorized($response, $result, $showDialog);
|
||||
}
|
||||
|
||||
if ($result->isSecondStepRequired()) {
|
||||
$this->handleSecondStepRequired($response, $result);
|
||||
}
|
||||
|
||||
return AuthResult::createNotResolved();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{string, string}
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function decodeAuthorizationString(string $string): array
|
||||
{
|
||||
/** @var string $stringDecoded */
|
||||
$stringDecoded = base64_decode($string);
|
||||
|
||||
if (!str_contains($stringDecoded, ':')) {
|
||||
throw new BadRequest("Auth: Bad authorization string provided.");
|
||||
}
|
||||
|
||||
[$username, $password] = explode(':', $stringDecoded, 2);
|
||||
|
||||
$username = trim($username);
|
||||
$password = trim($password);
|
||||
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
private function handleSecondStepRequired(Response $response, Result $result): void
|
||||
{
|
||||
$response->setStatus(401);
|
||||
$response->setHeader('X-Status-Reason', 'second-step-required');
|
||||
|
||||
$bodyData = [
|
||||
'status' => $result->getStatus(),
|
||||
'message' => $result->getMessage(),
|
||||
'view' => $result->getView(),
|
||||
'token' => $result->getToken(),
|
||||
'data' => $result->getData(),
|
||||
];
|
||||
|
||||
$response->writeBody(Json::encode($bodyData));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function handleException(Response $response, Exception $e): void
|
||||
{
|
||||
if (
|
||||
$e instanceof BadRequest ||
|
||||
$e instanceof ServiceUnavailable ||
|
||||
$e instanceof Forbidden
|
||||
) {
|
||||
$reason = $e->getMessage();
|
||||
|
||||
if ($reason) {
|
||||
$response->setHeader('X-Status-Reason', $e->getMessage());
|
||||
}
|
||||
|
||||
$response->setStatus($e->getCode());
|
||||
|
||||
if ($e->getBody()) {
|
||||
$response->writeBody($e->getBody());
|
||||
}
|
||||
|
||||
if ($e->getMessage()) {
|
||||
$this->log->notice("Auth exception: {message}", ['message' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
private function handleUnauthorized(Response $response, ?Result $result, bool $showDialog): void
|
||||
{
|
||||
if ($showDialog) {
|
||||
$response->setHeader('WWW-Authenticate', 'Basic realm=""');
|
||||
}
|
||||
|
||||
if ($result && $result->getFailReason() === Result\FailReason::ERROR) {
|
||||
$response = $response->setHeader('X-Status-Reason', 'error');
|
||||
}
|
||||
|
||||
$response->setStatus(401);
|
||||
}
|
||||
|
||||
private function isXMLHttpRequest(Request $request): bool
|
||||
{
|
||||
if (strtolower($request->getHeader('X-Requested-With') ?? '') == 'xmlhttprequest') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function obtainAuthenticationMethodFromRequest(Request $request): ?string
|
||||
{
|
||||
if ($request->hasHeader(HeaderKey::AUTHORIZATION)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$paramsList = array_values(array_filter(
|
||||
$this->configDataProvider->getLoginMetadataParamsList(),
|
||||
function ($params) use ($request): bool {
|
||||
$header = $params->getCredentialsHeader();
|
||||
|
||||
if (!$header || !$params->isApi()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $request->hasHeader($header);
|
||||
}
|
||||
));
|
||||
|
||||
if (count($paramsList)) {
|
||||
return $paramsList[0]->getMethod();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{?string, ?string}
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function obtainUsernamePasswordFromRequest(Request $request): array
|
||||
{
|
||||
if ($request->hasHeader(HeaderKey::AUTHORIZATION)) {
|
||||
$headerValue = $request->getHeader(HeaderKey::AUTHORIZATION) ?? '';
|
||||
|
||||
return $this->decodeAuthorizationString($headerValue);
|
||||
}
|
||||
|
||||
if (
|
||||
$request->getServerParam('PHP_AUTH_USER') &&
|
||||
$request->getServerParam('PHP_AUTH_PW')
|
||||
) {
|
||||
$username = $request->getServerParam('PHP_AUTH_USER');
|
||||
$password = $request->getServerParam('PHP_AUTH_PW');
|
||||
|
||||
if (is_string($username)) {
|
||||
$username = trim($username);
|
||||
}
|
||||
|
||||
if (is_string($password)) {
|
||||
$password = trim($password);
|
||||
}
|
||||
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
$cgiAuthString = $request->getHeader('Http-Espo-Cgi-Auth') ??
|
||||
$request->getHeader('Redirect-Http-Espo-Cgi-Auth');
|
||||
|
||||
if ($cgiAuthString) {
|
||||
[$username, $password] = $this->decodeAuthorizationString(substr($cgiAuthString, 6));
|
||||
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
private function obtainTokenFromCookies(Request $request): ?string
|
||||
{
|
||||
return $request->getCookieParam('auth-token');
|
||||
}
|
||||
}
|
||||
91
application/Espo/Core/Api/AuthBuilder.php
Normal file
91
application/Espo/Core/Api/AuthBuilder.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?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\Api;
|
||||
|
||||
use Espo\Core\Authentication\Authentication;
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\Binding\ContextualBinder;
|
||||
use Espo\Core\InjectableFactory;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Builds Auth instance.
|
||||
*/
|
||||
class AuthBuilder
|
||||
{
|
||||
private bool $authRequired = false;
|
||||
private bool $isEntryPoint = false;
|
||||
private ?Authentication $authentication = null;
|
||||
|
||||
public function __construct(private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
public function setAuthentication(Authentication $authentication): self
|
||||
{
|
||||
$this->authentication = $authentication;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAuthRequired(bool $authRequired): self
|
||||
{
|
||||
$this->authRequired = $authRequired;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function forEntryPoint(): self
|
||||
{
|
||||
$this->isEntryPoint = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function build(): Auth
|
||||
{
|
||||
if (!$this->authentication) {
|
||||
throw new RuntimeException("Authentication is not set.");
|
||||
}
|
||||
|
||||
return $this->injectableFactory->createWithBinding(
|
||||
Auth::class,
|
||||
BindingContainerBuilder
|
||||
::create()
|
||||
->bindInstance(Authentication::class, $this->authentication)
|
||||
->inContext(Auth::class, function (ContextualBinder $binder) {
|
||||
$binder
|
||||
->bindValue('$authRequired', $this->authRequired)
|
||||
->bindValue('$isEntryPoint', $this->isEntryPoint);
|
||||
})
|
||||
->build()
|
||||
);
|
||||
}
|
||||
}
|
||||
43
application/Espo/Core/Api/AuthBuilderFactory.php
Normal file
43
application/Espo/Core/Api/AuthBuilderFactory.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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\Api;
|
||||
|
||||
use Espo\Core\InjectableFactory;
|
||||
|
||||
class AuthBuilderFactory
|
||||
{
|
||||
public function __construct(private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
public function create(): AuthBuilder
|
||||
{
|
||||
return $this->injectableFactory->create(AuthBuilder::class);
|
||||
}
|
||||
}
|
||||
79
application/Espo/Core/Api/AuthResult.php
Normal file
79
application/Espo/Core/Api/AuthResult.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?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\Api;
|
||||
|
||||
/**
|
||||
* An authentication result.
|
||||
*/
|
||||
class AuthResult
|
||||
{
|
||||
private bool $isResolved = false;
|
||||
private bool $isResolvedUseNoAuth = false;
|
||||
|
||||
public static function createResolved(): self
|
||||
{
|
||||
$obj = new self();
|
||||
|
||||
$obj->isResolved = true;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public static function createResolvedUseNoAuth(): self
|
||||
{
|
||||
$obj = new self();
|
||||
|
||||
$obj->isResolved = true;
|
||||
$obj->isResolvedUseNoAuth = true;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public static function createNotResolved(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logged in successfully.
|
||||
*/
|
||||
public function isResolved(): bool
|
||||
{
|
||||
return $this->isResolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* No need to log in.
|
||||
*/
|
||||
public function isResolvedUseNoAuth(): bool
|
||||
{
|
||||
return $this->isResolvedUseNoAuth;
|
||||
}
|
||||
}
|
||||
90
application/Espo/Core/Api/ControllerActionHandler.php
Normal file
90
application/Espo/Core/Api/ControllerActionHandler.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?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\Api;
|
||||
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ControllerActionHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private string $controllerName,
|
||||
private string $actionName,
|
||||
private ProcessData $processData,
|
||||
private ResponseWrapper $responseWrapped,
|
||||
private ControllerActionProcessor $controllerActionProcessor,
|
||||
private Config $config
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$requestWrapped = new RequestWrapper(
|
||||
$request,
|
||||
$this->processData->getBasePath(),
|
||||
$this->processData->getRouteParams()
|
||||
);
|
||||
|
||||
$this->beforeProceed();
|
||||
|
||||
$responseWrapped = $this->controllerActionProcessor->process(
|
||||
$this->controllerName,
|
||||
$this->actionName,
|
||||
$requestWrapped,
|
||||
$this->responseWrapped
|
||||
);
|
||||
|
||||
$this->afterProceed($responseWrapped);
|
||||
|
||||
return $responseWrapped->toPsr7();
|
||||
}
|
||||
|
||||
private function beforeProceed(): void
|
||||
{
|
||||
$this->responseWrapped->setHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
private function afterProceed(Response $responseWrapped): void
|
||||
{
|
||||
$responseWrapped
|
||||
->setHeader('X-App-Timestamp', (string) ($this->config->get('appTimestamp') ?? '0'))
|
||||
->setHeader('Expires', '0')
|
||||
->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT')
|
||||
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
|
||||
}
|
||||
}
|
||||
217
application/Espo/Core/Api/ControllerActionProcessor.php
Normal file
217
application/Espo/Core/Api/ControllerActionProcessor.php
Normal file
@@ -0,0 +1,217 @@
|
||||
<?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\Api;
|
||||
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\NotFoundSilent;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\ClassFinder;
|
||||
use Espo\Core\Utils\Json;
|
||||
|
||||
use ReflectionClass;
|
||||
use ReflectionNamedType;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Creates controller instances and processes actions.
|
||||
*/
|
||||
class ControllerActionProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private ClassFinder $classFinder
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function process(
|
||||
string $controllerName,
|
||||
string $actionName,
|
||||
Request $request,
|
||||
ResponseWrapper $response
|
||||
): ResponseWrapper {
|
||||
|
||||
$controller = $this->createController($controllerName);
|
||||
|
||||
$requestMethod = $request->getMethod();
|
||||
|
||||
if (
|
||||
$actionName == 'index' &&
|
||||
property_exists($controller, 'defaultAction')
|
||||
) {
|
||||
$actionName = $controller::$defaultAction ?? 'index';
|
||||
}
|
||||
|
||||
$actionMethodName = 'action' . ucfirst($actionName);
|
||||
|
||||
$fullActionMethodName = strtolower($requestMethod) . ucfirst($actionMethodName);
|
||||
|
||||
$primaryActionMethodName = method_exists($controller, $fullActionMethodName) ?
|
||||
$fullActionMethodName :
|
||||
$actionMethodName;
|
||||
|
||||
if (!method_exists($controller, $primaryActionMethodName)) {
|
||||
throw new NotFoundSilent(
|
||||
"Action $requestMethod '$actionName' does not exist in controller '$controllerName'.");
|
||||
}
|
||||
|
||||
if ($this->useShortParamList($controller, $primaryActionMethodName)) {
|
||||
$result = $controller->$primaryActionMethodName($request, $response) ?? null;
|
||||
|
||||
$this->handleResult($response, $result);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Below is a legacy way.
|
||||
|
||||
$data = $request->getBodyContents();
|
||||
|
||||
if ($data && $this->getRequestContentType($request) === 'application/json') {
|
||||
$data = json_decode($data);
|
||||
}
|
||||
|
||||
$params = $request->getRouteParams();
|
||||
|
||||
$beforeMethodName = 'before' . ucfirst($actionName);
|
||||
|
||||
if (method_exists($controller, $beforeMethodName)) {
|
||||
$controller->$beforeMethodName($params, $data, $request, $response);
|
||||
}
|
||||
|
||||
$result = $controller->$primaryActionMethodName($params, $data, $request, $response) ?? null;
|
||||
|
||||
$afterMethodName = 'after' . ucfirst($actionName);
|
||||
|
||||
if (method_exists($controller, $afterMethodName)) {
|
||||
$controller->$afterMethodName($params, $data, $request, $response);
|
||||
}
|
||||
|
||||
$this->handleResult($response, $result);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $result
|
||||
*/
|
||||
private function handleResult(Response $response, $result): void
|
||||
{
|
||||
$responseContents = $result;
|
||||
|
||||
if (
|
||||
is_int($result) ||
|
||||
is_float($result) ||
|
||||
is_array($result) ||
|
||||
is_bool($result) ||
|
||||
$result instanceof stdClass
|
||||
) {
|
||||
$responseContents = Json::encode($result);
|
||||
}
|
||||
|
||||
if (is_string($responseContents)) {
|
||||
$response->writeBody($responseContents);
|
||||
}
|
||||
}
|
||||
|
||||
private function useShortParamList(object $controller, string $methodName): bool
|
||||
{
|
||||
$class = new ReflectionClass($controller);
|
||||
|
||||
$method = $class->getMethod($methodName);
|
||||
$params = $method->getParameters();
|
||||
|
||||
if (count($params) === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = $params[0]->getType();
|
||||
|
||||
if (
|
||||
!$type instanceof ReflectionNamedType ||
|
||||
$type->isBuiltin()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var class-string $className */
|
||||
$className = $type->getName();
|
||||
|
||||
$firstParamClass = new ReflectionClass($className);
|
||||
|
||||
if (
|
||||
$firstParamClass->getName() === Request::class ||
|
||||
$firstParamClass->isSubclassOf(Request::class)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function getControllerClassName(string $name): string
|
||||
{
|
||||
$className = $this->classFinder->find('Controllers', $name);
|
||||
|
||||
if (!$className) {
|
||||
throw new NotFound("Controller '$name' does not exist.");
|
||||
}
|
||||
|
||||
if (!class_exists($className)) {
|
||||
throw new NotFound("Class not found for controller '$name'.");
|
||||
}
|
||||
|
||||
return $className;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function createController(string $name): object
|
||||
{
|
||||
return $this->injectableFactory->createWith($this->getControllerClassName($name), [
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getRequestContentType(Request $request): ?string
|
||||
{
|
||||
if ($request instanceof RequestWrapper) {
|
||||
return $request->getContentType();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
278
application/Espo/Core/Api/ErrorOutput.php
Normal file
278
application/Espo/Core/Api/ErrorOutput.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?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\Api;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\HasBody;
|
||||
use Espo\Core\Exceptions\HasLogLevel;
|
||||
use Espo\Core\Exceptions\HasLogMessage;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Utils\Log;
|
||||
|
||||
use LogicException;
|
||||
use Psr\Log\LogLevel;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Processes an error output. If an exception occurred, it will be passed to here.
|
||||
*/
|
||||
class ErrorOutput
|
||||
{
|
||||
/** @var array<int, string> */
|
||||
private $errorDescriptions = [
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Page Not Found',
|
||||
409 => 'Conflict',
|
||||
500 => 'Internal Server Error',
|
||||
503 => 'Service Unavailable',
|
||||
];
|
||||
|
||||
/** @var int[] */
|
||||
private $allowedStatusCodeList = [
|
||||
200,
|
||||
201,
|
||||
400,
|
||||
401,
|
||||
403,
|
||||
404,
|
||||
409,
|
||||
500,
|
||||
503,
|
||||
];
|
||||
|
||||
/** @var class-string<Throwable>[] */
|
||||
private array $printStatusReasonExceptionClassNameList = [
|
||||
Error::class,
|
||||
Forbidden::class,
|
||||
Conflict::class,
|
||||
BadRequest::class,
|
||||
NotFound::class,
|
||||
];
|
||||
|
||||
public function __construct(private Log $log)
|
||||
{}
|
||||
|
||||
public function process(
|
||||
Request $request,
|
||||
Response $response,
|
||||
Throwable $exception,
|
||||
?string $route = null
|
||||
): void {
|
||||
|
||||
$this->processInternal($request, $response, $exception, $route);
|
||||
}
|
||||
|
||||
public function processWithBodyPrinting(
|
||||
Request $request,
|
||||
Response $response,
|
||||
Throwable $exception,
|
||||
?string $route = null
|
||||
): void {
|
||||
|
||||
$this->processInternal($request, $response, $exception, $route, true);
|
||||
}
|
||||
|
||||
private function processInternal(
|
||||
Request $request,
|
||||
Response $response,
|
||||
Throwable $exception,
|
||||
?string $route = null,
|
||||
bool $toPrintBody = false
|
||||
): void {
|
||||
|
||||
$message = $exception->getMessage();
|
||||
|
||||
if ($exception->getPrevious() && $exception->getPrevious()->getMessage()) {
|
||||
$message .= " " . $exception->getPrevious()->getMessage();
|
||||
}
|
||||
|
||||
$statusCode = $exception->getCode();
|
||||
|
||||
if ($exception instanceof HasLogMessage) {
|
||||
$message = $exception->getLogMessage();
|
||||
}
|
||||
|
||||
if ($route) {
|
||||
$this->processRoute($route, $request, $exception);
|
||||
}
|
||||
|
||||
$level = $this->getLevel($exception);
|
||||
|
||||
$this->log->log($level, $message, [
|
||||
'exception' => $exception,
|
||||
'request' => $request,
|
||||
]);
|
||||
|
||||
if (!in_array($statusCode, $this->allowedStatusCodeList)) {
|
||||
$statusCode = 500;
|
||||
}
|
||||
|
||||
$response->setStatus($statusCode);
|
||||
|
||||
if ($this->toPrintExceptionStatusReason($exception)) {
|
||||
$response->setHeader('X-Status-Reason', $this->stripInvalidCharactersFromHeaderValue($message));
|
||||
}
|
||||
|
||||
if ($exception instanceof HasBody && $this->exceptionHasBody($exception)) {
|
||||
$response->writeBody($exception->getBody() ?? '');
|
||||
|
||||
$toPrintBody = false;
|
||||
}
|
||||
|
||||
if ($toPrintBody) {
|
||||
$codeDescription = $this->getCodeDescription($statusCode);
|
||||
|
||||
$statusText = isset($codeDescription) ?
|
||||
$statusCode . ' '. $codeDescription :
|
||||
'HTTP ' . $statusCode;
|
||||
|
||||
if ($message) {
|
||||
$message = htmlspecialchars($message);
|
||||
}
|
||||
|
||||
$response->writeBody(self::generateErrorBody($statusText, $message));
|
||||
}
|
||||
}
|
||||
|
||||
private function exceptionHasBody(Throwable $exception): bool
|
||||
{
|
||||
if (!$exception instanceof HasBody) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$exceptionBody = $exception->getBody();
|
||||
|
||||
return $exceptionBody !== null;
|
||||
}
|
||||
|
||||
private function getCodeDescription(int $statusCode): ?string
|
||||
{
|
||||
if (isset($this->errorDescriptions[$statusCode])) {
|
||||
return $this->errorDescriptions[$statusCode];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function clearPasswords(string $string): string
|
||||
{
|
||||
return preg_replace('/"(.*password.*)":".*"/i', '"$1":"*****"', $string) ?? $string;
|
||||
}
|
||||
|
||||
private static function generateErrorBody(string $header, string $text): string
|
||||
{
|
||||
$body = "<h1>" . $header . "</h1>";
|
||||
$body .= $text;
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
private function stripInvalidCharactersFromHeaderValue(string $value): string
|
||||
{
|
||||
$pattern = "/[^ \t\x21-\x7E\x80-\xFF]/";
|
||||
|
||||
/** @var string */
|
||||
return preg_replace($pattern, ' ', $value);
|
||||
}
|
||||
|
||||
private function processRoute(string $route, Request $request, Throwable $exception): void
|
||||
{
|
||||
$requestBodyString = $this->clearPasswords($request->getBodyContents() ?? '');
|
||||
|
||||
$message = $exception->getMessage();
|
||||
|
||||
if ($exception->getPrevious() && $exception->getPrevious()->getMessage()) {
|
||||
$message .= " " . $exception->getPrevious()->getMessage();
|
||||
}
|
||||
|
||||
$statusCode = $exception->getCode();
|
||||
|
||||
$routeParams = $request->getRouteParams();
|
||||
|
||||
$logMessage = "API ($statusCode) ";
|
||||
|
||||
$logMessageItemList = [];
|
||||
|
||||
if ($message) {
|
||||
$logMessageItemList[] = $message;
|
||||
}
|
||||
|
||||
$logMessageItemList[] = $request->getMethod() . ' ' . $request->getResourcePath();
|
||||
|
||||
if ($requestBodyString) {
|
||||
$logMessageItemList[] = "Input data: " . $requestBodyString;
|
||||
}
|
||||
|
||||
$logMessageItemList[] = "Route pattern: " . $route;
|
||||
|
||||
if (!empty($routeParams)) {
|
||||
$logMessageItemList[] = "Route params: " . print_r($routeParams, true);
|
||||
}
|
||||
|
||||
$logMessage .= implode("; ", $logMessageItemList);
|
||||
|
||||
$this->log->debug($logMessage);
|
||||
}
|
||||
|
||||
private function toPrintExceptionStatusReason(Throwable $exception): bool
|
||||
{
|
||||
foreach ($this->printStatusReasonExceptionClassNameList as $clasName) {
|
||||
|
||||
if ($exception instanceof ($clasName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getLevel(Throwable $exception): string
|
||||
{
|
||||
if ($exception instanceof HasLogLevel) {
|
||||
return $exception->getLogLevel();
|
||||
}
|
||||
|
||||
if ($exception instanceof LogicException) {
|
||||
return LogLevel::ALERT;
|
||||
}
|
||||
|
||||
if ($exception instanceof RuntimeException) {
|
||||
return LogLevel::CRITICAL;
|
||||
}
|
||||
|
||||
return LogLevel::ERROR;
|
||||
}
|
||||
}
|
||||
47
application/Espo/Core/Api/Method.php
Normal file
47
application/Espo/Core/Api/Method.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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\Api;
|
||||
|
||||
/**
|
||||
* @since 9.2.0
|
||||
*/
|
||||
class Method
|
||||
{
|
||||
public const HEAD = 'HEAD';
|
||||
public const GET = 'GET';
|
||||
public const POST = 'POST';
|
||||
public const PUT = 'PUT';
|
||||
public const PATCH = 'PATCH';
|
||||
public const DELETE = 'DELETE';
|
||||
public const PURGE = 'PURGE';
|
||||
public const OPTIONS = 'OPTIONS';
|
||||
public const TRACE = 'TRACE';
|
||||
public const CONNECT = 'CONNECT';
|
||||
}
|
||||
128
application/Espo/Core/Api/MiddlewareProvider.php
Normal file
128
application/Espo/Core/Api/MiddlewareProvider.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?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\Api;
|
||||
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class MiddlewareProvider
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return MiddlewareInterface[]
|
||||
*/
|
||||
public function getGlobalMiddlewareList(): array
|
||||
{
|
||||
return $this->createFromClassNameList($this->getGlobalMiddlewareClassNameList());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MiddlewareInterface[]
|
||||
*/
|
||||
public function getRouteMiddlewareList(Route $route): array
|
||||
{
|
||||
$key = strtolower($route->getMethod()) . '_' . $route->getRoute();
|
||||
|
||||
/** @var class-string<MiddlewareInterface>[] $classNameList */
|
||||
$classNameList = $this->metadata->get(['app', 'api', 'routeMiddlewareClassNameListMap', $key]) ?? [];
|
||||
|
||||
return $this->createFromClassNameList($classNameList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MiddlewareInterface[]
|
||||
*/
|
||||
public function getActionMiddlewareList(Route $route): array
|
||||
{
|
||||
$key = strtolower($route->getMethod()) . '_' . $route->getRoute();
|
||||
|
||||
/** @var class-string<MiddlewareInterface>[] $classNameList */
|
||||
$classNameList = $this->metadata->get(['app', 'api', 'actionMiddlewareClassNameListMap', $key]) ?? [];
|
||||
|
||||
return $this->createFromClassNameList($classNameList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MiddlewareInterface[]
|
||||
*/
|
||||
public function getControllerMiddlewareList(string $controller): array
|
||||
{
|
||||
/** @var class-string<MiddlewareInterface>[] $classNameList */
|
||||
$classNameList = $this->metadata
|
||||
->get(['app', 'api', 'controllerMiddlewareClassNameListMap', $controller]) ?? [];
|
||||
|
||||
return $this->createFromClassNameList($classNameList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MiddlewareInterface[]
|
||||
*/
|
||||
public function getControllerActionMiddlewareList(string $method, string $controller, string $action): array
|
||||
{
|
||||
$key = $controller . '_' . strtolower($method) . '_' . $action;
|
||||
|
||||
/** @var class-string<MiddlewareInterface>[] $classNameList */
|
||||
$classNameList = $this->metadata
|
||||
->get(['app', 'api', 'controllerActionMiddlewareClassNameListMap', $key]) ?? [];
|
||||
|
||||
return $this->createFromClassNameList($classNameList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<MiddlewareInterface>[]
|
||||
*/
|
||||
private function getGlobalMiddlewareClassNameList(): array
|
||||
{
|
||||
return $this->metadata->get(['app', 'api', 'globalMiddlewareClassNameList']) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<MiddlewareInterface>[] $classNameList
|
||||
* @return MiddlewareInterface[]
|
||||
*/
|
||||
private function createFromClassNameList(array $classNameList): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
foreach ($classNameList as $className) {
|
||||
$list[] = $this->injectableFactory->create($className);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
66
application/Espo/Core/Api/ProcessData.php
Normal file
66
application/Espo/Core/Api/ProcessData.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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\Api;
|
||||
|
||||
class ProcessData
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $routeParams
|
||||
*/
|
||||
public function __construct(
|
||||
private Route $route,
|
||||
private string $basePath,
|
||||
private array $routeParams
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return Route
|
||||
*/
|
||||
public function getRoute(): Route
|
||||
{
|
||||
return $this->route;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBasePath(): string
|
||||
{
|
||||
return $this->basePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRouteParams(): array
|
||||
{
|
||||
return $this->routeParams;
|
||||
}
|
||||
}
|
||||
128
application/Espo/Core/Api/Request.php
Normal file
128
application/Espo/Core/Api/Request.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?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\Api;
|
||||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Representation of an HTTP request.
|
||||
*/
|
||||
interface Request
|
||||
{
|
||||
/**
|
||||
* Whether a query parameter is set.
|
||||
*/
|
||||
public function hasQueryParam(string $name): bool;
|
||||
|
||||
/**
|
||||
* Get a query parameter.
|
||||
*/
|
||||
public function getQueryParam(string $name): ?string;
|
||||
|
||||
/**
|
||||
* Get all query parameters.
|
||||
*
|
||||
* @return array<string, string|array<scalar, mixed>>
|
||||
*/
|
||||
public function getQueryParams(): array;
|
||||
|
||||
/**
|
||||
* Whether a route parameter is set.
|
||||
*/
|
||||
public function hasRouteParam(string $name): bool;
|
||||
|
||||
/**
|
||||
* Get a route parameter.
|
||||
*/
|
||||
public function getRouteParam(string $name): ?string;
|
||||
|
||||
/**
|
||||
* Get all route parameters.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRouteParams(): array;
|
||||
|
||||
/**
|
||||
* Get a header value. Multiple values will be concatenated with a comma.
|
||||
*/
|
||||
public function getHeader(string $name): ?string;
|
||||
|
||||
/**
|
||||
* Whether a header is set.
|
||||
*/
|
||||
public function hasHeader(string $name): bool;
|
||||
|
||||
/**
|
||||
* Get a header values as an array.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderAsArray(string $name): array;
|
||||
|
||||
/**
|
||||
* Get a request method.
|
||||
*/
|
||||
public function getMethod(): string;
|
||||
|
||||
/**
|
||||
* Get Uri.
|
||||
*/
|
||||
public function getUri(): UriInterface;
|
||||
|
||||
/**
|
||||
* Get a relative path of a request (w/o base path).
|
||||
*/
|
||||
public function getResourcePath(): string;
|
||||
|
||||
/**
|
||||
* Get body contents.
|
||||
*/
|
||||
public function getBodyContents(): ?string;
|
||||
|
||||
/**
|
||||
* Get a parsed body. If JSON array is passed, then will be converted to `{"list": ARRAY}`.
|
||||
*/
|
||||
public function getParsedBody(): stdClass;
|
||||
|
||||
/**
|
||||
* Get a cookie param value.
|
||||
*/
|
||||
public function getCookieParam(string $name): ?string;
|
||||
|
||||
/**
|
||||
* Get a server param value.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getServerParam(string $name);
|
||||
}
|
||||
133
application/Espo/Core/Api/RequestNull.php
Normal file
133
application/Espo/Core/Api/RequestNull.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?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\Api;
|
||||
|
||||
use Espo\Core\Api\Request as ApiRequest;
|
||||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Slim\Psr7\Factory\UriFactory;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* An empty stub for Request.
|
||||
*/
|
||||
class RequestNull implements ApiRequest
|
||||
{
|
||||
public function hasQueryParam(string $name): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function getQueryParam(string $name): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getQueryParams(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function hasRouteParam(string $name): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getRouteParam(string $name): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getRouteParams(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getHeader(string $name): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function hasHeader(string $name): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderAsArray(string $name): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getUri(): UriInterface
|
||||
{
|
||||
return (new UriFactory())->createUri();
|
||||
}
|
||||
|
||||
public function getResourcePath(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getBodyContents(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getParsedBody(): stdClass
|
||||
{
|
||||
return (object) [];
|
||||
}
|
||||
|
||||
public function getCookieParam(string $name): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getServerParam(string $name)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
292
application/Espo/Core/Api/RequestWrapper.php
Normal file
292
application/Espo/Core/Api/RequestWrapper.php
Normal file
@@ -0,0 +1,292 @@
|
||||
<?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\Api;
|
||||
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Api\Request as ApiRequest;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface as Psr7Request;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Adapter for PSR-7 request interface.
|
||||
*/
|
||||
class RequestWrapper implements ApiRequest
|
||||
{
|
||||
private ?stdClass $parsedBody = null;
|
||||
|
||||
/**
|
||||
* @param array<string, string> $routeParams
|
||||
*/
|
||||
public function __construct(
|
||||
private Psr7Request $psr7Request,
|
||||
private string $basePath = '',
|
||||
private array $routeParams = []
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get a route or query parameter. Route params have a higher priority.
|
||||
*
|
||||
* @todo Don't support NULL $name.
|
||||
* @deprecated As of v6.0. Use getQueryParam & getRouteParam. Left for backward compatibility.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(?string $name = null)
|
||||
{
|
||||
if (is_null($name)) {
|
||||
return array_merge(
|
||||
$this->getQueryParams(),
|
||||
$this->routeParams
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->hasRouteParam($name)) {
|
||||
return $this->getRouteParam($name);
|
||||
}
|
||||
|
||||
return $this->psr7Request->getQueryParams()[$name] ?? null;
|
||||
}
|
||||
|
||||
public function hasRouteParam(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->routeParams);
|
||||
}
|
||||
|
||||
public function getRouteParam(string $name): ?string
|
||||
{
|
||||
return $this->routeParams[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRouteParams(): array
|
||||
{
|
||||
return $this->routeParams;
|
||||
}
|
||||
|
||||
public function hasQueryParam(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->psr7Request->getQueryParams());
|
||||
}
|
||||
|
||||
public function getQueryParam(string $name): ?string
|
||||
{
|
||||
$value = $this->psr7Request->getQueryParams()[$name] ?? null;
|
||||
|
||||
if (!is_string($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getQueryParams(): array
|
||||
{
|
||||
return $this->psr7Request->getQueryParams();
|
||||
}
|
||||
|
||||
public function getHeader(string $name): ?string
|
||||
{
|
||||
if (!$this->psr7Request->hasHeader($name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->psr7Request->getHeaderLine($name);
|
||||
}
|
||||
|
||||
public function hasHeader(string $name): bool
|
||||
{
|
||||
return $this->psr7Request->hasHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderAsArray(string $name): array
|
||||
{
|
||||
if (!$this->psr7Request->hasHeader($name)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->psr7Request->getHeader($name);
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->psr7Request->getMethod();
|
||||
}
|
||||
|
||||
public function getContentType(): ?string
|
||||
{
|
||||
if (!$this->hasHeader('Content-Type')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$contentType = explode(
|
||||
';',
|
||||
$this->psr7Request->getHeader('Content-Type')[0]
|
||||
)[0];
|
||||
|
||||
return strtolower($contentType);
|
||||
}
|
||||
|
||||
public function getBodyContents(): ?string
|
||||
{
|
||||
$contents = $this->psr7Request->getBody()->getContents();
|
||||
|
||||
$this->psr7Request->getBody()->rewind();
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function getParsedBody(): stdClass
|
||||
{
|
||||
if ($this->parsedBody === null) {
|
||||
$this->initParsedBody();
|
||||
}
|
||||
|
||||
if ($this->parsedBody === null) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
return Util::cloneObject($this->parsedBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function initParsedBody(): void
|
||||
{
|
||||
$contents = $this->getBodyContents();
|
||||
|
||||
$contentType = $this->getContentType();
|
||||
|
||||
if ($contentType === 'application/json' && $contents) {
|
||||
$parsedBody = Json::decode($contents);
|
||||
|
||||
if (is_array($parsedBody)) {
|
||||
$parsedBody = (object) [
|
||||
'list' => $parsedBody,
|
||||
];
|
||||
}
|
||||
|
||||
if (!$parsedBody instanceof stdClass) {
|
||||
throw new BadRequest("Body is not a JSON object.");
|
||||
}
|
||||
|
||||
$this->parsedBody = $parsedBody;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
in_array($contentType, ['application/x-www-form-urlencoded', 'multipart/form-data']) &&
|
||||
$contents
|
||||
) {
|
||||
$parsedBody = $this->psr7Request->getParsedBody();
|
||||
|
||||
if (is_array($parsedBody)) {
|
||||
$this->parsedBody = (object) $parsedBody;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($parsedBody instanceof stdClass) {
|
||||
$this->parsedBody = $parsedBody;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->parsedBody = (object) [];
|
||||
}
|
||||
|
||||
public function getCookieParam(string $name): ?string
|
||||
{
|
||||
$params = $this->psr7Request->getCookieParams();
|
||||
|
||||
return $params[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getServerParam(string $name)
|
||||
{
|
||||
$params = $this->psr7Request->getServerParams();
|
||||
|
||||
return $params[$name] ?? null;
|
||||
}
|
||||
|
||||
public function getUri(): UriInterface
|
||||
{
|
||||
return $this->psr7Request->getUri();
|
||||
}
|
||||
|
||||
public function getResourcePath(): string
|
||||
{
|
||||
$path = $this->psr7Request->getUri()->getPath();
|
||||
|
||||
return substr($path, strlen($this->basePath));
|
||||
}
|
||||
|
||||
public function isGet(): bool
|
||||
{
|
||||
return $this->getMethod() === Method::GET;
|
||||
}
|
||||
|
||||
public function isPut(): bool
|
||||
{
|
||||
return $this->getMethod() === Method::PUT;
|
||||
}
|
||||
|
||||
public function isPost(): bool
|
||||
{
|
||||
return $this->getMethod() === Method::POST;
|
||||
}
|
||||
|
||||
public function isPatch(): bool
|
||||
{
|
||||
return $this->getMethod() === Method::PATCH;
|
||||
}
|
||||
|
||||
public function isDelete(): bool
|
||||
{
|
||||
return $this->getMethod() === Method::DELETE;
|
||||
}
|
||||
}
|
||||
102
application/Espo/Core/Api/Response.php
Normal file
102
application/Espo/Core/Api/Response.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?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\Api;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Representation of an HTTP response. An instance is mutable.
|
||||
*/
|
||||
interface Response
|
||||
{
|
||||
/**
|
||||
* Get a status code.
|
||||
*/
|
||||
public function getStatusCode(): int;
|
||||
|
||||
/**
|
||||
* Get a status reason phrase.
|
||||
*/
|
||||
public function getReasonPhrase(): string;
|
||||
|
||||
/**
|
||||
* Set a status code.
|
||||
*/
|
||||
public function setStatus(int $code, ?string $reason = null): self;
|
||||
|
||||
/**
|
||||
* Set a specific header.
|
||||
*/
|
||||
public function setHeader(string $name, string $value): self;
|
||||
|
||||
/**
|
||||
* Add a specific header.
|
||||
*/
|
||||
public function addHeader(string $name, string $value): self;
|
||||
|
||||
/**
|
||||
* Get a header value.
|
||||
*/
|
||||
public function getHeader(string $name): ?string;
|
||||
|
||||
/**
|
||||
* Whether a header is set.
|
||||
*/
|
||||
public function hasHeader(string $name): bool;
|
||||
|
||||
/**
|
||||
* Get all set header names.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderNames(): array;
|
||||
|
||||
/**
|
||||
* Get a header values as an array.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderAsArray(string $name): array;
|
||||
|
||||
/**
|
||||
* Write a body.
|
||||
*/
|
||||
public function writeBody(string $string): self;
|
||||
|
||||
/**
|
||||
* Set a body.
|
||||
*/
|
||||
public function setBody(StreamInterface $body): self;
|
||||
|
||||
/**
|
||||
* Get a body.
|
||||
*/
|
||||
public function getBody(): StreamInterface;
|
||||
}
|
||||
59
application/Espo/Core/Api/ResponseComposer.php
Normal file
59
application/Espo/Core/Api/ResponseComposer.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?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\Api;
|
||||
|
||||
use Slim\Psr7\Factory\ResponseFactory;
|
||||
use Espo\Core\Utils\Json;
|
||||
use stdClass;
|
||||
|
||||
class ResponseComposer
|
||||
{
|
||||
/**
|
||||
* Compose a JSON response.
|
||||
*
|
||||
* @param array<string|int, mixed>|stdClass|scalar|null $data A data to encode.
|
||||
*/
|
||||
public static function json(mixed $data): Response
|
||||
{
|
||||
return self::empty()
|
||||
->writeBody(Json::encode($data))
|
||||
->setHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose an empty response.
|
||||
*/
|
||||
public static function empty(): Response
|
||||
{
|
||||
$psr7Response = (new ResponseFactory())->createResponse();
|
||||
|
||||
return new ResponseWrapper($psr7Response);
|
||||
}
|
||||
}
|
||||
136
application/Espo/Core/Api/ResponseWrapper.php
Normal file
136
application/Espo/Core/Api/ResponseWrapper.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?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\Api;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Psr7Response;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
use Espo\Core\Api\Response as ApiResponse;
|
||||
|
||||
/**
|
||||
* Adapter for PSR-7 response interface.
|
||||
*/
|
||||
class ResponseWrapper implements ApiResponse
|
||||
{
|
||||
public function __construct(private Psr7Response $psr7Response)
|
||||
{
|
||||
// Slim adds Authorization header. It's not needed.
|
||||
$this->psr7Response = $this->psr7Response->withoutHeader('Authorization');
|
||||
}
|
||||
|
||||
public function setStatus(int $code, ?string $reason = null): Response
|
||||
{
|
||||
$this->psr7Response = $this->psr7Response->withStatus($code, $reason ?? '');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->psr7Response->getStatusCode();
|
||||
}
|
||||
|
||||
public function getReasonPhrase(): string
|
||||
{
|
||||
return $this->psr7Response->getReasonPhrase();
|
||||
}
|
||||
|
||||
public function setHeader(string $name, string $value): Response
|
||||
{
|
||||
$this->psr7Response = $this->psr7Response->withHeader($name, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addHeader(string $name, string $value): Response
|
||||
{
|
||||
$this->psr7Response = $this->psr7Response->withAddedHeader($name, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHeader(string $name): ?string
|
||||
{
|
||||
if (!$this->psr7Response->hasHeader($name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->psr7Response->getHeaderLine($name);
|
||||
}
|
||||
|
||||
public function hasHeader(string $name): bool
|
||||
{
|
||||
return $this->psr7Response->hasHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderAsArray(string $name): array
|
||||
{
|
||||
if (!$this->psr7Response->hasHeader($name)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->psr7Response->getHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderNames(): array
|
||||
{
|
||||
return array_keys($this->psr7Response->getHeaders());
|
||||
}
|
||||
|
||||
public function writeBody(string $string): Response
|
||||
{
|
||||
$this->psr7Response->getBody()->write($string);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setBody(StreamInterface $body): Response
|
||||
{
|
||||
$this->psr7Response = $this->psr7Response->withBody($body);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBody(): StreamInterface
|
||||
{
|
||||
return $this->psr7Response->getBody();
|
||||
}
|
||||
|
||||
public function toPsr7(): Psr7Response
|
||||
{
|
||||
return $this->psr7Response;
|
||||
}
|
||||
}
|
||||
92
application/Espo/Core/Api/Route.php
Normal file
92
application/Espo/Core/Api/Route.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?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\Api;
|
||||
|
||||
class Route
|
||||
{
|
||||
private string $method;
|
||||
|
||||
/**
|
||||
* @param array<string, string> $params
|
||||
* @param ?class-string<Action> $actionClassName
|
||||
*/
|
||||
public function __construct(
|
||||
string $method,
|
||||
private string $route,
|
||||
private string $adjustedRoute,
|
||||
private array $params,
|
||||
private bool $noAuth,
|
||||
private ?string $actionClassName
|
||||
) {
|
||||
$this->method = strtoupper($method);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?class-string<Action>
|
||||
*/
|
||||
public function getActionClassName(): ?string
|
||||
{
|
||||
return $this->actionClassName;
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a route.
|
||||
*/
|
||||
public function getRoute(): string
|
||||
{
|
||||
return $this->route;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an adjusted route for FastRoute.
|
||||
*/
|
||||
public function getAdjustedRoute(): string
|
||||
{
|
||||
return $this->adjustedRoute;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getParams(): array
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
public function noAuth(): bool
|
||||
{
|
||||
return $this->noAuth;
|
||||
}
|
||||
}
|
||||
76
application/Espo/Core/Api/Route/RouteParamsFetcher.php
Normal file
76
application/Espo/Core/Api/Route/RouteParamsFetcher.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?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\Api\Route;
|
||||
|
||||
use Espo\Core\Api\Route;
|
||||
|
||||
class RouteParamsFetcher
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function fetch(Route $item, array $args): array
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$routeParams = $item->getParams();
|
||||
|
||||
$setKeyList = [];
|
||||
|
||||
foreach (array_keys($routeParams) as $key) {
|
||||
$value = $routeParams[$key];
|
||||
|
||||
$paramName = $key;
|
||||
|
||||
if ($value[0] === ':') {
|
||||
$realKey = substr($value, 1);
|
||||
|
||||
$params[$paramName] = $args[$realKey];
|
||||
|
||||
$setKeyList[] = $realKey;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$params[$paramName] = $value;
|
||||
}
|
||||
|
||||
foreach ($args as $key => $value) {
|
||||
if (in_array($key, $setKeyList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$params[$key] = $value;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
275
application/Espo/Core/Api/RouteProcessor.php
Normal file
275
application/Espo/Core/Api/RouteProcessor.php
Normal file
@@ -0,0 +1,275 @@
|
||||
<?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\Api;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Authentication\AuthenticationFactory;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\ApplicationUser;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Psr7Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Psr7Request;
|
||||
|
||||
use Slim\MiddlewareDispatcher;
|
||||
|
||||
use Throwable;
|
||||
use LogicException;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Processes routes. Handles authentication. Obtains a controller name, action, body from a request.
|
||||
* Then processes a controller action or an action.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class RouteProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private AuthenticationFactory $authenticationFactory,
|
||||
private AuthBuilderFactory $authBuilderFactory,
|
||||
private ErrorOutput $errorOutput,
|
||||
private Config $config,
|
||||
private Log $log,
|
||||
private ApplicationUser $applicationUser,
|
||||
private ControllerActionProcessor $actionProcessor,
|
||||
private MiddlewareProvider $middlewareProvider,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
public function process(
|
||||
ProcessData $processData,
|
||||
Psr7Request $request,
|
||||
Psr7Response $response
|
||||
): Psr7Response {
|
||||
|
||||
$requestWrapped = new RequestWrapper($request, $processData->getBasePath(), $processData->getRouteParams());
|
||||
$responseWrapped = new ResponseWrapper($response);
|
||||
|
||||
try {
|
||||
return $this->processInternal(
|
||||
$processData,
|
||||
$request,
|
||||
$requestWrapped,
|
||||
$responseWrapped
|
||||
);
|
||||
} catch (Exception $exception) {
|
||||
$this->handleException(
|
||||
$exception,
|
||||
$requestWrapped,
|
||||
$responseWrapped,
|
||||
$processData->getRoute()->getAdjustedRoute()
|
||||
);
|
||||
|
||||
return $responseWrapped->toPsr7();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function processInternal(
|
||||
ProcessData $processData,
|
||||
Psr7Request $psrRequest,
|
||||
RequestWrapper $request,
|
||||
ResponseWrapper $response
|
||||
): Psr7Response {
|
||||
|
||||
$authRequired = !$processData->getRoute()->noAuth();
|
||||
|
||||
$apiAuth = $this->authBuilderFactory
|
||||
->create()
|
||||
->setAuthentication($this->authenticationFactory->create())
|
||||
->setAuthRequired($authRequired)
|
||||
->build();
|
||||
|
||||
$authResult = $apiAuth->process($request, $response);
|
||||
|
||||
if (!$authResult->isResolved()) {
|
||||
return $response->toPsr7();
|
||||
}
|
||||
|
||||
if ($authResult->isResolvedUseNoAuth()) {
|
||||
$this->applicationUser->setupSystemUser();
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
$response = $this->processAfterAuth($processData, $psrRequest, $response);
|
||||
|
||||
ob_clean();
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function processAfterAuth(
|
||||
ProcessData $processData,
|
||||
Psr7Request $request,
|
||||
ResponseWrapper $responseWrapped
|
||||
): Psr7Response {
|
||||
|
||||
$actionClassName = $processData->getRoute()->getActionClassName();
|
||||
|
||||
if ($actionClassName) {
|
||||
return $this->processAction($actionClassName, $processData, $request, $responseWrapped);
|
||||
}
|
||||
|
||||
return $this->processControllerAction($processData, $request, $responseWrapped);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Action> $actionClassName
|
||||
*/
|
||||
private function processAction(
|
||||
string $actionClassName,
|
||||
ProcessData $processData,
|
||||
Psr7Request $request,
|
||||
ResponseWrapper $responseWrapped
|
||||
): Psr7Response {
|
||||
|
||||
/** @var Action $action */
|
||||
$action = $this->injectableFactory->create($actionClassName);
|
||||
|
||||
$handler = new ActionHandler(
|
||||
action: $action,
|
||||
processData: $processData,
|
||||
config: $this->config,
|
||||
);
|
||||
|
||||
$dispatcher = new MiddlewareDispatcher($handler);
|
||||
|
||||
foreach ($this->middlewareProvider->getActionMiddlewareList($processData->getRoute()) as $middleware) {
|
||||
$dispatcher->addMiddleware($middleware);
|
||||
}
|
||||
|
||||
$response = $dispatcher->handle($request);
|
||||
|
||||
// Apply headers added by the authentication.
|
||||
foreach ($responseWrapped->getHeaderNames() as $name) {
|
||||
$response = $response->withHeader($name, $responseWrapped->getHeaderAsArray($name));
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function processControllerAction(
|
||||
ProcessData $processData,
|
||||
Psr7Request $request,
|
||||
ResponseWrapper $responseWrapped
|
||||
): Psr7Response {
|
||||
|
||||
$controller = $this->getControllerName($processData);
|
||||
$action = $processData->getRouteParams()['action'] ?? null;
|
||||
$method = $request->getMethod();
|
||||
|
||||
if (!$action) {
|
||||
$crudMethodActionMap = $this->config->get('crud') ?? [];
|
||||
$action = $crudMethodActionMap[strtolower($method)] ?? null;
|
||||
|
||||
if (!$action) {
|
||||
throw new BadRequest("No action for method `$method`.");
|
||||
}
|
||||
}
|
||||
|
||||
$handler = new ControllerActionHandler(
|
||||
controllerName: $controller,
|
||||
actionName: $action,
|
||||
processData: $processData,
|
||||
responseWrapped: $responseWrapped,
|
||||
controllerActionProcessor: $this->actionProcessor,
|
||||
config: $this->config,
|
||||
);
|
||||
|
||||
$dispatcher = new MiddlewareDispatcher($handler);
|
||||
|
||||
$this->addControllerMiddlewares($dispatcher, $method, $controller, $action);
|
||||
|
||||
return $dispatcher->handle($request);
|
||||
}
|
||||
|
||||
private function getControllerName(ProcessData $processData): string
|
||||
{
|
||||
$controllerName = $processData->getRouteParams()['controller'] ?? null;
|
||||
|
||||
if (!$controllerName) {
|
||||
throw new LogicException("Route doesn't have specified controller.");
|
||||
}
|
||||
|
||||
return ucfirst($controllerName);
|
||||
}
|
||||
|
||||
private function handleException(
|
||||
Exception $exception,
|
||||
Request $request,
|
||||
Response $response,
|
||||
string $route
|
||||
): void {
|
||||
|
||||
try {
|
||||
$this->errorOutput->process($request, $response, $exception, $route);
|
||||
} catch (Throwable $exceptionAnother) {
|
||||
$this->log->error($exceptionAnother->getMessage());
|
||||
|
||||
$response->setStatus(500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MiddlewareDispatcher<null> $dispatcher
|
||||
*/
|
||||
private function addControllerMiddlewares(
|
||||
MiddlewareDispatcher $dispatcher,
|
||||
string $method,
|
||||
string $controller,
|
||||
string $action
|
||||
): void {
|
||||
|
||||
$controllerActionMiddlewareList = $this->middlewareProvider
|
||||
->getControllerActionMiddlewareList($method, $controller, $action);
|
||||
|
||||
foreach ($controllerActionMiddlewareList as $middleware) {
|
||||
$dispatcher->addMiddleware($middleware);
|
||||
}
|
||||
|
||||
$controllerMiddlewareList = $this->middlewareProvider
|
||||
->getControllerMiddlewareList($controller);
|
||||
|
||||
foreach ($controllerMiddlewareList as $middleware) {
|
||||
$dispatcher->addMiddleware($middleware);
|
||||
}
|
||||
}
|
||||
}
|
||||
154
application/Espo/Core/Api/Starter.php
Normal file
154
application/Espo/Core/Api/Starter.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?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\Api;
|
||||
|
||||
use Espo\Core\Api\Route\RouteParamsFetcher;
|
||||
use Espo\Core\Utils\Config\SystemConfig;
|
||||
use Espo\Core\Utils\Route as RouteUtil;
|
||||
use Espo\Core\Utils\Log;
|
||||
|
||||
use Slim\App as SlimApp;
|
||||
use Slim\Exception\HttpBadRequestException;
|
||||
use Slim\Factory\AppFactory as SlimAppFactory;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Psr7Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Psr7Request;
|
||||
|
||||
/**
|
||||
* API request processing entry point.
|
||||
*/
|
||||
class Starter
|
||||
{
|
||||
private string $routeCacheFile = 'data/cache/application/slim-routes.php';
|
||||
|
||||
public function __construct(
|
||||
private RouteProcessor $routeProcessor,
|
||||
private RouteUtil $routeUtil,
|
||||
private RouteParamsFetcher $routeParamsFetcher,
|
||||
private MiddlewareProvider $middlewareProvider,
|
||||
private Log $log,
|
||||
private SystemConfig $systemConfig,
|
||||
?string $routeCacheFile = null
|
||||
) {
|
||||
$this->routeCacheFile = $routeCacheFile ?? $this->routeCacheFile;
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
$slim = SlimAppFactory::create();
|
||||
|
||||
if (RouteUtil::isBadUri()) {
|
||||
$this->processError($slim);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->systemConfig->useCache()) {
|
||||
$slim->getRouteCollector()->setCacheFile($this->routeCacheFile);
|
||||
}
|
||||
|
||||
$slim->setBasePath(RouteUtil::detectBasePath());
|
||||
$this->addGlobalMiddlewares($slim);
|
||||
$slim->addRoutingMiddleware();
|
||||
$this->addRoutes($slim);
|
||||
$slim->addErrorMiddleware(false, true, true, $this->log);
|
||||
|
||||
$slim->run();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SlimApp<ContainerInterface|null> $slim
|
||||
*/
|
||||
private function addGlobalMiddlewares(SlimApp $slim): void
|
||||
{
|
||||
foreach ($this->middlewareProvider->getGlobalMiddlewareList() as $middleware) {
|
||||
$slim->add($middleware);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SlimApp<ContainerInterface|null> $slim
|
||||
*/
|
||||
private function addRoutes(SlimApp $slim): void
|
||||
{
|
||||
$routeList = $this->routeUtil->getFullList();
|
||||
|
||||
foreach ($routeList as $item) {
|
||||
$this->addRoute($slim, $item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SlimApp<ContainerInterface|null> $slim
|
||||
*/
|
||||
private function addRoute(SlimApp $slim, Route $item): void
|
||||
{
|
||||
$slimRoute = $slim->map(
|
||||
[$item->getMethod()],
|
||||
$item->getAdjustedRoute(),
|
||||
function (Psr7Request $request, Psr7Response $response, array $args) use ($slim, $item) {
|
||||
$routeParams = $this->routeParamsFetcher->fetch($item, $args);
|
||||
|
||||
$processData = new ProcessData(
|
||||
route: $item,
|
||||
basePath: $slim->getBasePath(),
|
||||
routeParams: $routeParams,
|
||||
);
|
||||
|
||||
return $this->routeProcessor->process($processData, $request, $response);
|
||||
}
|
||||
);
|
||||
|
||||
$middlewareList = $this->middlewareProvider->getRouteMiddlewareList($item);
|
||||
|
||||
foreach ($middlewareList as $middleware) {
|
||||
$slimRoute->addMiddleware($middleware);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SlimApp<ContainerInterface|null> $slim
|
||||
*/
|
||||
private function processError(SlimApp $slim): void
|
||||
{
|
||||
$slim->add(function (Psr7Request $request): Psr7Response {
|
||||
throw new HttpBadRequestException($request, 'Malformed request path.');
|
||||
});
|
||||
|
||||
$slim->addErrorMiddleware(
|
||||
displayErrorDetails: false,
|
||||
logErrors: false,
|
||||
logErrorDetails: false,
|
||||
);
|
||||
|
||||
$slim->run();
|
||||
}
|
||||
}
|
||||
85
application/Espo/Core/Api/Util.php
Normal file
85
application/Espo/Core/Api/Util.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?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\Api;
|
||||
|
||||
use Espo\Core\Utils\Config;
|
||||
use stdClass;
|
||||
|
||||
class Util
|
||||
{
|
||||
public function __construct(private Config $config) {}
|
||||
|
||||
public static function cloneObject(stdClass $source): stdClass
|
||||
{
|
||||
$cloned = (object) [];
|
||||
|
||||
foreach (get_object_vars($source) as $k => $v) {
|
||||
$cloned->$k = self::cloneObjectItem($v);
|
||||
}
|
||||
|
||||
return $cloned;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $item
|
||||
* @return mixed
|
||||
*/
|
||||
private static function cloneObjectItem($item)
|
||||
{
|
||||
if (is_array($item)) {
|
||||
$cloned = [];
|
||||
|
||||
foreach ($item as $v) {
|
||||
$cloned[] = self::cloneObjectItem($v);
|
||||
}
|
||||
|
||||
return $cloned;
|
||||
}
|
||||
|
||||
if ($item instanceof stdClass) {
|
||||
return self::cloneObject($item);
|
||||
}
|
||||
|
||||
if (is_object($item)) {
|
||||
return clone $item;
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
public function obtainIpFromRequest(Request $request): ?string
|
||||
{
|
||||
// Do not add support of any more parameters here.
|
||||
|
||||
$param = $this->config->get('ipAddressServerParam') ?? 'REMOTE_ADDR';
|
||||
|
||||
return $request->getServerParam($param);
|
||||
}
|
||||
}
|
||||
167
application/Espo/Core/Application.php
Normal file
167
application/Espo/Core/Application.php
Normal file
@@ -0,0 +1,167 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\Application\ApplicationParams;
|
||||
use Espo\Core\Application\Runner;
|
||||
use Espo\Core\Application\RunnerParameterized;
|
||||
use Espo\Core\Container\ContainerBuilder;
|
||||
use Espo\Core\Application\RunnerRunner;
|
||||
use Espo\Core\Application\Runner\Params as RunnerParams;
|
||||
use Espo\Core\Application\Exceptions\RunnerException;
|
||||
use Espo\Core\Utils\Autoload;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\ClientManager;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* A central access point of the application.
|
||||
*/
|
||||
class Application
|
||||
{
|
||||
protected Container $container;
|
||||
|
||||
public function __construct(
|
||||
?ApplicationParams $params = null,
|
||||
) {
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
$this->initContainer($params);
|
||||
$this->initAutoloads();
|
||||
$this->initPreloads();
|
||||
}
|
||||
|
||||
protected function initContainer(?ApplicationParams $params): void
|
||||
{
|
||||
$container = (new ContainerBuilder())
|
||||
->withParams($params)
|
||||
->build();
|
||||
|
||||
if (!$container instanceof Container) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an application runner.
|
||||
*
|
||||
* @param class-string<Runner|RunnerParameterized> $className A runner class name.
|
||||
* @param ?RunnerParams $params Runner parameters.
|
||||
*/
|
||||
public function run(string $className, ?RunnerParams $params = null): void
|
||||
{
|
||||
$runnerRunner = $this->getInjectableFactory()->create(RunnerRunner::class);
|
||||
|
||||
try {
|
||||
$runnerRunner->run($className, $params);
|
||||
} catch (RunnerException $e) {
|
||||
die($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the application is installed.
|
||||
*/
|
||||
public function isInstalled(): bool
|
||||
{
|
||||
return $this->getConfig()->get('isInstalled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the service container.
|
||||
*/
|
||||
public function getContainer(): Container
|
||||
{
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
protected function getInjectableFactory(): InjectableFactory
|
||||
{
|
||||
return $this->container->getByClass(InjectableFactory::class);
|
||||
}
|
||||
|
||||
protected function getApplicationUser(): ApplicationUser
|
||||
{
|
||||
return $this->container->getByClass(ApplicationUser::class);
|
||||
}
|
||||
|
||||
protected function getClientManager(): ClientManager
|
||||
{
|
||||
return $this->container->getByClass(ClientManager::class);
|
||||
}
|
||||
|
||||
protected function getMetadata(): Metadata
|
||||
{
|
||||
return $this->container->getByClass(Metadata::class);
|
||||
}
|
||||
|
||||
protected function getConfig(): Config
|
||||
{
|
||||
return $this->container->getByClass(Config::class);
|
||||
}
|
||||
|
||||
protected function initAutoloads(): void
|
||||
{
|
||||
$autoload = $this->getInjectableFactory()->create(Autoload::class);
|
||||
|
||||
$autoload->register();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize services that has the 'preload' parameter.
|
||||
*/
|
||||
protected function initPreloads(): void
|
||||
{
|
||||
foreach ($this->getMetadata()->get(['app', 'containerServices']) ?? [] as $name => $defs) {
|
||||
if ($defs['preload'] ?? false) {
|
||||
$this->container->get($name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a base path of an index file related to the application directory. Used for a portal.
|
||||
*/
|
||||
public function setClientBasePath(string $basePath): void
|
||||
{
|
||||
$this->getClientManager()->setBasePath($basePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the system user. The system user is used when no user is logged in.
|
||||
*/
|
||||
public function setupSystemUser(): void
|
||||
{
|
||||
$this->getApplicationUser()->setupSystemUser();
|
||||
}
|
||||
}
|
||||
40
application/Espo/Core/Application/ApplicationParams.php
Normal file
40
application/Espo/Core/Application/ApplicationParams.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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\Application;
|
||||
|
||||
/**
|
||||
* @since 9.2.0
|
||||
*/
|
||||
readonly class ApplicationParams
|
||||
{
|
||||
public function __construct(
|
||||
public bool $noErrorHandler = false,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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\Application\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class RunnerException extends Exception
|
||||
{}
|
||||
43
application/Espo/Core/Application/Runner.php
Normal file
43
application/Espo/Core/Application/Runner.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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\Application;
|
||||
|
||||
use Espo\Core\Application\Exceptions\RunnerException;
|
||||
|
||||
/**
|
||||
* Runs an application.
|
||||
*/
|
||||
interface Runner
|
||||
{
|
||||
/**
|
||||
* @throws RunnerException
|
||||
*/
|
||||
public function run(): void;
|
||||
}
|
||||
89
application/Espo/Core/Application/Runner/Params.php
Normal file
89
application/Espo/Core/Application/Runner/Params.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?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\Application\Runner;
|
||||
|
||||
/**
|
||||
* Parameters for an application runner.
|
||||
*/
|
||||
class Params
|
||||
{
|
||||
/** @var array<string, mixed> */
|
||||
private $data = [];
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
/**
|
||||
* Get a parameter value.
|
||||
*/
|
||||
public function get(string $name): mixed
|
||||
{
|
||||
return $this->data[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a parameter is set.
|
||||
*/
|
||||
public function has(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone with a parameter value.
|
||||
*/
|
||||
public function with(string $name, mixed $value): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->data[$name] = $value;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from an associative array.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$obj = new self();
|
||||
$obj->data = $data;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty instance.
|
||||
*/
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
}
|
||||
40
application/Espo/Core/Application/RunnerParameterized.php
Normal file
40
application/Espo/Core/Application/RunnerParameterized.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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\Application;
|
||||
|
||||
use Espo\Core\Application\Runner\Params;
|
||||
|
||||
/**
|
||||
* Runs an application with parameters.
|
||||
*/
|
||||
interface RunnerParameterized
|
||||
{
|
||||
public function run(Params $params): void;
|
||||
}
|
||||
86
application/Espo/Core/Application/RunnerRunner.php
Normal file
86
application/Espo/Core/Application/RunnerRunner.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?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\Application;
|
||||
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\ApplicationUser;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Application\Exceptions\RunnerException;
|
||||
use Espo\Core\Application\Runner\Params;
|
||||
|
||||
use ReflectionClass;
|
||||
|
||||
/**
|
||||
* Runs a runner.
|
||||
*/
|
||||
class RunnerRunner
|
||||
{
|
||||
public function __construct(
|
||||
private Log $log,
|
||||
private ApplicationUser $applicationUser,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param class-string<Runner|RunnerParameterized> $className
|
||||
* @throws RunnerException
|
||||
*/
|
||||
public function run(string $className, ?Params $params = null): void
|
||||
{
|
||||
if (!class_exists($className)) {
|
||||
$this->log->error("Application runner '$className' does not exist.");
|
||||
|
||||
throw new RunnerException();
|
||||
}
|
||||
|
||||
$class = new ReflectionClass($className);
|
||||
|
||||
if (
|
||||
$class->getStaticPropertyValue('cli', false) &&
|
||||
!str_starts_with(php_sapi_name() ?: '', 'cli')
|
||||
) {
|
||||
throw new RunnerException("Can be run only via CLI.");
|
||||
}
|
||||
|
||||
if ($class->getStaticPropertyValue('setupSystemUser', false)) {
|
||||
$this->applicationUser->setupSystemUser();
|
||||
}
|
||||
|
||||
$runner = $this->injectableFactory->create($className);
|
||||
|
||||
if ($runner instanceof RunnerParameterized) {
|
||||
$runner->run($params ?? Params::create());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$runner->run();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user