Initial commit

This commit is contained in:
root
2026-01-19 17:44:46 +01:00
commit 823af8b11d
8721 changed files with 1130846 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
<?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\Portal;
use Espo\ORM\Entity;
use Espo\Entities\User;
use Espo\Core\Acl as BaseAcl;
class Acl extends BaseAcl
{
public function __construct(AclManager $aclManager, User $user)
{
parent::__construct($aclManager, $user);
}
/**
* Whether 'read' access is set to 'account' for a specific scope.
*/
public function checkReadOnlyAccount(string $scope): bool
{
/** @var AclManager $aclManager */
$aclManager = $this->aclManager;
return $aclManager->checkReadOnlyAccount($this->user, $scope);
}
/**
* Whether 'read' access is set to 'contact' for a specific scope.
*/
public function checkReadOnlyContact(string $scope): bool
{
/** @var AclManager $aclManager */
$aclManager = $this->aclManager;
return $aclManager->checkReadOnlyContact($this->user, $scope);
}
/**
* Check whether an entity belongs to a user account.
*/
public function checkOwnershipAccount(Entity $entity): bool
{
/** @var AclManager $aclManager */
$aclManager = $this->aclManager;
return $aclManager->checkOwnershipAccount($this->user, $entity);
}
/**
* Check whether an entity belongs to a user contact.
*/
public function checkOwnershipContact(Entity $entity): bool
{
/** @var AclManager $aclManager */
$aclManager = $this->aclManager;
return $aclManager->checkOwnershipContact($this->user, $entity);
}
/**
* @deprecate
*/
public function checkInAccount(Entity $entity): bool
{
return $this->checkOwnershipAccount($entity);
}
/**
* @deprecate
*/
public function checkIsOwnContact(Entity $entity): bool
{
return $this->checkOwnershipContact($entity);
}
}

View File

@@ -0,0 +1,108 @@
<?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\Portal\Acl\AccessChecker;
use Espo\Core\Acl\AccessChecker;
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\Portal\Acl\DefaultAccessChecker;
use Espo\Core\Portal\AclManager as PortalAclManager;
use Espo\Core\Utils\Metadata;
class AccessCheckerFactory
{
/** @var class-string<AccessChecker> */
private $defaultClassName = DefaultAccessChecker::class;
public function __construct(
private Metadata $metadata,
private InjectableFactory $injectableFactory
) {}
/**
* Create an access checker.
*
* @throws NotImplemented
*/
public function create(string $scope, PortalAclManager $aclManager): AccessChecker
{
$className = $this->getClassName($scope);
$bindingContainer = $this->createBindingContainer($className, $aclManager, $scope);
return $this->injectableFactory->createWithBinding($className, $bindingContainer);
}
/**
* @return class-string<AccessChecker>
*/
private function getClassName(string $scope): string
{
/** @var ?class-string<AccessChecker> $className1 */
$className1 = $this->metadata->get(['aclDefs', $scope, 'portalAccessCheckerClassName']);
if ($className1) {
return $className1;
}
if (!$this->metadata->get(['scopes', $scope])) {
throw new NotImplemented();
}
return $this->defaultClassName;
}
/**
* @param class-string<AccessChecker> $className
*/
private function createBindingContainer(
string $className,
PortalAclManager $aclManager,
string $scope
): BindingContainer {
$bindingData = new BindingData();
$binder = new Binder($bindingData);
$binder
->bindInstance(PortalAclManager::class, $aclManager)
->bindInstance(AclManager::class, $aclManager);
$binder
->for($className)
->bindValue('$entityType', $scope);
return new BindingContainer($bindingData);
}
}

View 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\Portal\Acl\AccessChecker;
use Espo\Core\Acl\ScopeData;
use Espo\Core\Portal\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_ACCOUNT || $level === Table::LEVEL_CONTACT) {
if ($checkerData->isOwn()) {
return true;
}
}
if ($level === Table::LEVEL_ACCOUNT || $level === Table::LEVEL_CONTACT) {
if ($checkerData->inContact()) {
return true;
}
}
if ($level === Table::LEVEL_ACCOUNT) {
if ($checkerData->inAccount()) {
return true;
}
}
return false;
}
}

View 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\Portal\Acl\AccessChecker;
use Closure;
/**
* Scope checker data.
*/
class ScopeCheckerData
{
public function __construct(
private Closure $isOwnChecker,
private Closure $inAccountChecker,
private Closure $inContactChecker
) {}
public function isOwn(): bool
{
return ($this->isOwnChecker)();
}
public function inAccount(): bool
{
return ($this->inAccountChecker)();
}
public function inContact(): bool
{
return ($this->inContactChecker)();
}
public static function createBuilder(): ScopeCheckerDataBuilder
{
return new ScopeCheckerDataBuilder();
}
}

View File

@@ -0,0 +1,143 @@
<?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\Portal\Acl\AccessChecker;
use Closure;
/**
* Builds scope checker data.
*/
class ScopeCheckerDataBuilder
{
private Closure $isOwnChecker;
private Closure $inAccountChecker;
private Closure $inContactChecker;
public function __construct()
{
$this->isOwnChecker = function (): bool {
return false;
};
$this->inAccountChecker = function (): bool {
return false;
};
$this->inContactChecker = function (): bool {
return 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 setInAccount(bool $value): self
{
if ($value) {
$this->inAccountChecker = function (): bool {
return true;
};
return $this;
}
$this->inAccountChecker = function (): bool {
return false;
};
return $this;
}
public function setInContact(bool $value): self
{
if ($value) {
$this->inContactChecker = function (): bool {
return true;
};
return $this;
}
$this->inContactChecker = function (): bool {
return false;
};
return $this;
}
/**
* @param Closure(): bool $checker
*/
public function setIsOwnChecker(Closure $checker): self
{
$this->isOwnChecker = $checker;
return $this;
}
/**
* @param Closure(): bool $checker
*/
public function setInAccountChecker(Closure $checker): self
{
$this->inAccountChecker = $checker;
return $this;
}
/**
* @param Closure(): bool $checker
*/
public function setInContactChecker(Closure $checker): self
{
$this->inContactChecker = $checker;
return $this;
}
public function build(): ScopeCheckerData
{
return new ScopeCheckerData($this->isOwnChecker, $this->inAccountChecker, $this->inContactChecker);
}
}

View File

@@ -0,0 +1,157 @@
<?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\Portal\Acl;
use Espo\Entities\User;
use Espo\ORM\Entity;
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\ScopeData;
use Espo\Core\Portal\Acl\AccessChecker\ScopeChecker;
use Espo\Core\Portal\Acl\AccessChecker\ScopeCheckerData;
use Espo\Core\Portal\AclManager as PortalAclManager;
/**
* A default implementation for access checking for portal.
*
* @implements AccessEntityCreateChecker<Entity>
* @implements AccessEntityReadChecker<Entity>
* @implements AccessEntityEditChecker<Entity>
* @implements AccessEntityDeleteChecker<Entity>
* @implements AccessEntityStreamChecker<Entity>
*/
class DefaultAccessChecker implements
AccessEntityCreateChecker,
AccessEntityReadChecker,
AccessEntityEditChecker,
AccessEntityDeleteChecker,
AccessEntityStreamChecker
{
public function __construct(
private PortalAclManager $aclManager,
private ScopeChecker $scopeChecker
) {}
private function checkEntity(User $user, Entity $entity, ScopeData $data, string $action): bool
{
$checkerData = ScopeCheckerData
::createBuilder()
->setIsOwnChecker(
function () use ($user, $entity): bool {
return $this->aclManager->checkOwnershipOwn($user, $entity);
}
)
->setInAccountChecker(
function () use ($user, $entity): bool {
return $this->aclManager->checkOwnershipAccount($user, $entity);
}
)
->setInContactChecker(
function () use ($user, $entity): bool {
return $this->aclManager->checkOwnershipContact($user, $entity);
}
)
->build();
return $this->scopeChecker->check($data, $action, $checkerData);
}
private function checkScope(User $user, ScopeData $data, ?string $action = null): bool
{
$checkerData = ScopeCheckerData
::createBuilder()
->setIsOwn(true)
->setInAccount(true)
->setInContact(true)
->build();
return $this->scopeChecker->check($data, $action, $checkerData);
}
public function check(User $user, ScopeData $data): bool
{
return $this->checkScope($user, $data);
}
public function checkCreate(User $user, ScopeData $data): bool
{
return $this->checkScope($user, $data, Table::ACTION_CREATE);
}
public function checkRead(User $user, ScopeData $data): bool
{
return $this->checkScope($user, $data, Table::ACTION_READ);
}
public function checkEdit(User $user, ScopeData $data): bool
{
return $this->checkScope($user, $data, Table::ACTION_EDIT);
}
public function checkDelete(User $user, ScopeData $data): bool
{
return $this->checkScope($user, $data, Table::ACTION_DELETE);
}
public function checkStream(User $user, ScopeData $data): bool
{
return $this->checkScope($user, $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
{
return $this->checkEntity($user, $entity, $data, Table::ACTION_DELETE);
}
}

View 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\Portal\Acl;
use Espo\Core\Field\Link;
use Espo\Core\Field\LinkParent;
use Espo\Core\Name\Field;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Portal\Acl\OwnershipChecker\MetadataProvider;
use Espo\Modules\Crm\Entities\Account;
use Espo\Modules\Crm\Entities\Contact;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\Entities\User;
use Espo\Core\Acl\OwnershipOwnChecker;
use Espo\ORM\Type\RelationType;
/**
* A default implementation for ownership checking for portal.
*
* @implements OwnershipOwnChecker<\Espo\Core\ORM\Entity>
* @implements OwnershipAccountChecker<\Espo\Core\ORM\Entity>
* @implements OwnershipContactChecker<\Espo\Core\ORM\Entity>
*/
class DefaultOwnershipChecker implements
OwnershipOwnChecker,
OwnershipAccountChecker,
OwnershipContactChecker
{
private const ATTR_CREATED_BY_ID = Field::CREATED_BY . 'Id';
public function __construct(
private EntityManager $entityManager,
private MetadataProvider $metadataProvider,
) {}
public function checkOwn(User $user, Entity $entity): bool
{
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 checkAccount(User $user, Entity $entity): bool
{
$linkDefs = $this->metadataProvider->getAccountLink($entity->getEntityType());
if (!$linkDefs) {
return false;
}
$link = $linkDefs->getName();
$accountIds = $user->getAccounts()->getIdList();
if ($accountIds === []) {
return false;
}
$fieldDefs = $this->entityManager
->getDefs()
->getEntity($entity->getEntityType())
->tryGetField($link);
if (
$linkDefs->getType() === RelationType::BELONGS_TO &&
$fieldDefs?->getType() === FieldType::LINK
) {
$setAccountLink = $entity->getValueObject($link);
if (!$setAccountLink instanceof Link) {
return false;
}
return in_array($setAccountLink->getId(), $accountIds);
}
if (
$linkDefs->getType() === RelationType::BELONGS_TO_PARENT &&
$fieldDefs?->getType() === FieldType::LINK_PARENT
) {
$setLink = $entity->getValueObject($link);
if (!$setLink instanceof LinkParent || $setLink->getEntityType() !== Account::ENTITY_TYPE) {
return false;
}
return in_array($setLink->getId(), $accountIds);
}
foreach ($accountIds as $accountId) {
$isRelated = $this->entityManager
->getRelation($entity, $link)
->isRelatedById($accountId);
if ($isRelated) {
return true;
}
}
return false;
}
public function checkContact(User $user, Entity $entity): bool
{
$linkDefs = $this->metadataProvider->getContactLink($entity->getEntityType());
if (!$linkDefs) {
return false;
}
$link = $linkDefs->getName();
$contactId = $user->getContactId();
if (!$contactId) {
return false;
}
$fieldDefs = $this->entityManager
->getDefs()
->getEntity($entity->getEntityType())
->tryGetField($link);
if (
$linkDefs->getType() === RelationType::BELONGS_TO &&
$fieldDefs?->getType() === FieldType::LINK
) {
$setContactLink = $entity->getValueObject($link);
if (!$setContactLink instanceof Link) {
return false;
}
return $setContactLink->getId() === $contactId;
}
if (
$linkDefs->getType() === RelationType::BELONGS_TO_PARENT &&
$fieldDefs?->getType() === FieldType::LINK_PARENT
) {
$setLink = $entity->getValueObject($link);
if (!$setLink instanceof LinkParent || $setLink->getEntityType() !== Contact::ENTITY_TYPE) {
return false;
}
return $setLink->getId() === $contactId;
}
return$this->entityManager
->getRelation($entity, $link)
->isRelatedById($contactId);
}
}

View 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\Portal\Acl\Map;
use Espo\Entities\Portal;
use Espo\Entities\User;
use Espo\Core\Acl\Map\CacheKeyProvider as CacheKeyProviderInterface;
class CacheKeyProvider implements CacheKeyProviderInterface
{
public function __construct(private User $user, private Portal $portal)
{}
public function get(): string
{
return 'aclPortalMap/' . $this->portal->getId() . '/' . $this->user->getId();
}
}

View 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\Portal\Acl\Map;
use Espo\Entities\Portal;
use Espo\Entities\User;
use Espo\Core\Acl\Map\CacheKeyProvider;
use Espo\Core\Acl\Map\Map;
use Espo\Core\Acl\Map\MetadataProvider;
use Espo\Core\Acl\Table;
use Espo\Core\Binding\Binder;
use Espo\Core\Binding\BindingContainer;
use Espo\Core\Binding\BindingData;
use Espo\Core\InjectableFactory;
use Espo\Core\Portal\Acl\Map\CacheKeyProvider as PortalCacheKeyProvider;
use Espo\Core\Portal\Acl\Map\MetadataProvider as PortalMetadataProvider;
use Espo\Core\Portal\Acl\Table as PortalTable;
class MapFactory
{
public function __construct(private InjectableFactory $injectableFactory)
{}
public function create(User $user, PortalTable $table, Portal $portal): Map
{
$bindingContainer = $this->createBindingContainer($user, $table, $portal);
return $this->injectableFactory->createWithBinding(Map::class, $bindingContainer);
}
private function createBindingContainer(User $user, PortalTable $table, Portal $portal): BindingContainer
{
$bindingData = new BindingData();
$binder = new Binder($bindingData);
$binder
->bindInstance(User::class, $user)
->bindInstance(Table::class, $table)
->bindInstance(Portal::class, $portal)
->bindImplementation(MetadataProvider::class, PortalMetadataProvider::class)
->bindImplementation(CacheKeyProvider::class, PortalCacheKeyProvider::class);
return new BindingContainer($bindingData);
}
}

View 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\Portal\Acl\Map;
use Espo\Core\Acl\Map\MetadataProvider as BaseMetadataProvider;
class MetadataProvider extends BaseMetadataProvider
{
protected string $type = 'aclPortal';
}

View 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\Portal\Acl;
use Espo\ORM\Entity;
use Espo\Entities\User;
use Espo\Core\Acl\OwnershipChecker;
/**
* @template TEntity of Entity
*/
interface OwnershipAccountChecker extends OwnershipChecker
{
/**
* Check whether an entity belongs to a portal user account.
*
* @param TEntity $entity
*/
public function checkAccount(User $user, Entity $entity): bool;
}

View 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\Portal\Acl\OwnershipChecker;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Defs;
use Espo\ORM\Defs\RelationDefs;
class MetadataProvider
{
public function __construct(
private Metadata $metadata,
private Defs $defs,
) {}
public function getAccountLink(string $entityType): ?RelationDefs
{
$link = $this->metadata->get("aclDefs.$entityType.accountLink");
if (!$link) {
return null;
}
return $this->defs->getEntity($entityType)->tryGetRelation($link);
}
public function getContactLink(string $entityType): ?RelationDefs
{
$link = $this->metadata->get("aclDefs.$entityType.contactLink");
if (!$link) {
return null;
}
return $this->defs->getEntity($entityType)->tryGetRelation($link);
}
}

View File

@@ -0,0 +1,109 @@
<?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\Portal\Acl\OwnershipChecker;
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\Portal\Acl\DefaultOwnershipChecker;
use Espo\Core\Portal\AclManager as PortalAclManager;
use Espo\Core\Utils\Metadata;
class OwnershipCheckerFactory
{
/** @var class-string<OwnershipChecker> */
private $defaultClassName = DefaultOwnershipChecker::class;
public function __construct(
private Metadata $metadata,
private InjectableFactory $injectableFactory
) {}
/**
* Create an ownership checker.
*
* @throws NotImplemented
*/
public function create(string $scope, PortalAclManager $aclManager): OwnershipChecker
{
$className = $this->getClassName($scope);
$bindingContainer = $this->createBindingContainer($className, $aclManager, $scope);
return $this->injectableFactory->createWithBinding($className, $bindingContainer);
}
/**
* @return class-string<OwnershipChecker>
*/
private function getClassName(string $scope): string
{
$className = $this->metadata->get(['aclDefs', $scope, 'portalOwnershipCheckerClassName']);
if ($className) {
/** @var class-string<OwnershipChecker> */
return $className;
}
if (!$this->metadata->get(['scopes', $scope])) {
throw new NotImplemented();
}
return $this->defaultClassName;
}
/**
* @param class-string<OwnershipChecker> $className
*/
private function createBindingContainer(
string $className,
PortalAclManager $aclManager,
string $scope
): BindingContainer {
$bindingData = new BindingData();
$binder = new Binder($bindingData);
$binder
->bindInstance(PortalAclManager::class, $aclManager)
->bindInstance(AclManager::class, $aclManager);
$binder
->for($className)
->bindValue('$entityType', $scope);
return new BindingContainer($bindingData);
}
}

View 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\Portal\Acl;
use Espo\ORM\Entity;
use Espo\Entities\User;
use Espo\Core\Acl\OwnershipChecker;
/**
* @template TEntity of Entity
*/
interface OwnershipContactChecker extends OwnershipChecker
{
/**
* Check whether an entity belongs to a portal user contact.
*
* @param TEntity $entity
*/
public function checkContact(User $user, Entity $entity): bool;
}

View 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\Portal\Acl;
use Espo\Core\Acl\Table\DefaultTable as BaseTable;
use stdClass;
class Table extends BaseTable
{
public const LEVEL_ACCOUNT = 'account';
public const LEVEL_CONTACT = 'contact';
protected string $type = 'aclPortal';
/**
* @var string[]
*/
protected $levelList = [
self::LEVEL_YES,
self::LEVEL_ALL,
self::LEVEL_ACCOUNT,
self::LEVEL_CONTACT,
self::LEVEL_OWN,
self::LEVEL_NO,
];
/**
* @return string[]
*/
protected function getScopeWithAclList(): array
{
$scopeList = [];
$scopes = $this->metadata->get('scopes');
foreach ($scopes as $scope => $item) {
if (empty($item['acl'])) {
continue;
}
if (empty($item['aclPortal'])) {
continue;
}
$scopeList[] = $scope;
}
return $scopeList;
}
protected function applyDefault(stdClass &$table, stdClass &$fieldTable): void
{
parent::applyDefault($table, $fieldTable);
foreach ($this->getScopeList() as $scope) {
if (!isset($table->$scope)) {
$table->$scope = false;
}
}
}
protected function applyDisabled(stdClass $table, stdClass $fieldTable): void
{
foreach ($this->getScopeList() as $scope) {
$item = $this->metadata->get(['scopes', $scope]) ?? [];
if (!empty($item['disabled']) || !empty($item['portalDisabled'])) {
$table->$scope = false;
unset($fieldTable->$scope);
}
}
}
}

View 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\Portal\Acl\Table;
use Espo\Entities\Portal;
use Espo\Entities\User;
use Espo\Core\Acl\Table\CacheKeyProvider as CacheKeyProviderInterface;
class CacheKeyProvider implements CacheKeyProviderInterface
{
public function __construct(private User $user, private Portal $portal)
{}
public function get(): string
{
return 'aclPortal/' . $this->portal->getId() . '/' . $this->user->getId();
}
}

View File

@@ -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\Portal\Acl\Table;
use Espo\ORM\EntityManager;
use Espo\Entities\Portal;
use Espo\Entities\PortalRole;
use Espo\Entities\User;
use Espo\Core\Acl\Table\Role;
use Espo\Core\Acl\Table\RoleEntityWrapper;
use Espo\Core\Acl\Table\RoleListProvider as RoleListProviderInterface;
class RoleListProvider implements RoleListProviderInterface
{
public function __construct(
private User $user,
private Portal $portal,
private EntityManager $entityManager
) {}
/**
* @return Role[]
*/
public function get(): array
{
$roleList = [];
/** @var iterable<PortalRole> $userRoleList */
$userRoleList = $this->entityManager
->getRDBRepository(User::ENTITY_TYPE)
->getRelation($this->user, 'portalRoles')
->find();
foreach ($userRoleList as $role) {
$roleList[] = $role;
}
/** @var iterable<PortalRole> $portalRoleList */
$portalRoleList = $this->entityManager
->getRDBRepository(Portal::ENTITY_TYPE)
->getRelation($this->portal, 'portalRoles')
->find();
foreach ($portalRoleList as $role) {
$roleList[] = $role;
}
return array_map(
function (PortalRole $role): RoleEntityWrapper {
return new RoleEntityWrapper($role);
},
$roleList
);
}
}

View 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\Portal\Acl\Table;
use Espo\Entities\Portal;
use Espo\Entities\User;
use Espo\Core\Acl\Table\CacheKeyProvider;
use Espo\Core\Acl\Table\RoleListProvider;
use Espo\Core\Binding\Binder;
use Espo\Core\Binding\BindingContainer;
use Espo\Core\Binding\BindingData;
use Espo\Core\InjectableFactory;
use Espo\Core\Portal\Acl\Table;
use Espo\Core\Portal\Acl\Table\CacheKeyProvider as PortalCacheKeyProvider;
use Espo\Core\Portal\Acl\Table\RoleListProvider as PortalRoleListProvider;
class TableFactory
{
public function __construct(private InjectableFactory $injectableFactory)
{}
/**
* Create a table.
*/
public function create(User $user, Portal $portal): Table
{
$bindingContainer = $this->createBindingContainer($user, $portal);
return $this->injectableFactory->createWithBinding(Table::class, $bindingContainer);
}
private function createBindingContainer(User $user, Portal $portal): BindingContainer
{
$bindingData = new BindingData();
$binder = new Binder($bindingData);
$binder
->bindInstance(User::class, $user)
->bindInstance(Portal::class, $portal)
->bindImplementation(RoleListProvider::class, PortalRoleListProvider::class)
->bindImplementation(CacheKeyProvider::class, PortalCacheKeyProvider::class);
return new BindingContainer($bindingData);
}
}

View 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\Portal\Acl\Traits;
use Espo\ORM\Entity;
use Espo\Entities\User;
use Espo\Core\Acl\ScopeData;
use Espo\Core\Portal\Acl\DefaultAccessChecker;
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);
}
}

View File

@@ -0,0 +1,341 @@
<?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\Portal;
use Espo\Core\Acl\Permission;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\Entities\Portal;
use Espo\Entities\User;
use Espo\Core\Acl\GlobalRestriction;
use Espo\Core\Acl\Map\Map;
use Espo\Core\Acl\OwnerUserFieldProvider;
use Espo\Core\Acl\Table;
use Espo\Core\AclManager as InternalAclManager;
use Espo\Core\Portal\Acl\AccessChecker\AccessCheckerFactory as PortalAccessCheckerFactory;
use Espo\Core\Portal\Acl\Map\MapFactory as PortalMapFactory;
use Espo\Core\Portal\Acl\OwnershipAccountChecker;
use Espo\Core\Portal\Acl\OwnershipChecker\OwnershipCheckerFactory as PortalOwnershipCheckerFactory;
use Espo\Core\Portal\Acl\OwnershipContactChecker;
use Espo\Core\Portal\Acl\Table as PortalTable;
use Espo\Core\Portal\Acl\Table\TableFactory as PortalTableFactory;
use stdClass;
use RuntimeException;
class AclManager extends InternalAclManager
{
/** @var class-string */
protected $userAclClassName = Acl::class;
private InternalAclManager $internalAclManager;
private ?Portal $portal = null;
private PortalTableFactory $portalTableFactory;
private PortalMapFactory $portalMapFactory;
public function __construct(
PortalAccessCheckerFactory $accessCheckerFactory,
PortalOwnershipCheckerFactory $ownershipCheckerFactory,
PortalTableFactory $portalTableFactory,
PortalMapFactory $portalMapFactory,
GlobalRestriction $globalRestriction,
OwnerUserFieldProvider $ownerUserFieldProvider,
EntityManager $entityManager,
InternalAclManager $internalAclManager
) {
$this->accessCheckerFactory = $accessCheckerFactory;
$this->ownershipCheckerFactory = $ownershipCheckerFactory;
$this->portalTableFactory = $portalTableFactory;
$this->portalMapFactory = $portalMapFactory;
$this->globalRestriction = $globalRestriction;
$this->ownerUserFieldProvider = $ownerUserFieldProvider;
$this->entityManager = $entityManager;
$this->internalAclManager = $internalAclManager;
}
public function setPortal(Portal $portal): void
{
$this->portal = $portal;
}
protected function getPortal(): Portal
{
if (!$this->portal) {
throw new RuntimeException("Portal is not set.");
}
return $this->portal;
}
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->portalTableFactory->create($user, $this->getPortal());
}
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)) {
/** @var PortalTable $table */
$table = $this->getTable($user);
$this->mapHashMap[$key] = $this->portalMapFactory->create($user, $table, $this->getPortal());
}
return $this->mapHashMap[$key];
}
public function getMapData(User $user): stdClass
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->getMapData($user);
}
return parent::getMapData($user);
}
public function getLevel(User $user, string $scope, string $action): string
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->getLevel($user, $scope, $action);
}
return parent::getLevel($user, $scope, $action);
}
public function getPermissionLevel(User $user, string $permission): string
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->getPermissionLevel($user, $permission);
}
return parent::getPermissionLevel($user, $permission);
}
public function checkReadOnlyTeam(User $user, string $scope): bool
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->checkReadOnlyTeam($user, $scope);
}
return false;
}
public function checkReadNo(User $user, string $scope): bool
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->checkReadNo($user, $scope);
}
return parent::checkReadNo($user, $scope);
}
public function checkReadOnlyOwn(User $user, string $scope): bool
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->checkReadOnlyOwn($user, $scope);
}
return parent::checkReadOnlyOwn($user, $scope);
}
public function checkReadAll(User $user, string $scope): bool
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->checkReadAll($user, $scope);
}
return parent::checkReadAll($user, $scope);
}
/**
* Whether 'read' access is set to 'account' for a specific scope.
*/
public function checkReadOnlyAccount(User $user, string $scope): bool
{
return $this->getLevel($user, $scope, Table::ACTION_READ) === PortalTable::LEVEL_ACCOUNT;
}
/**
* Whether 'read' access is set to 'contact' for a specific scope.
*/
public function checkReadOnlyContact(User $user, string $scope): bool
{
return $this->getLevel($user, $scope, Table::ACTION_READ)=== PortalTable::LEVEL_CONTACT;
}
public function check(User $user, $subject, ?string $action = null): bool
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->check($user, $subject, $action);
}
return parent::check($user, $subject, $action);
}
public function checkEntity(User $user, Entity $entity, string $action = Table::ACTION_READ): bool
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->checkEntity($user, $entity, $action);
}
return parent::checkEntity($user, $entity, $action);
}
public function checkUserPermission(User $user, $target, string $permissionType = Permission::USER): bool
{
return $this->internalAclManager->checkUserPermission($user, $target, $permissionType);
}
public function checkOwnershipOwn(User $user, Entity $entity): bool
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->checkOwnershipOwn($user, $entity);
}
return parent::checkOwnershipOwn($user, $entity);
}
public function checkOwnershipShared(User $user, Entity $entity, string $action): bool
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->checkOwnershipShared($user, $entity, $action);
}
return false;
}
public function checkOwnershipTeam(User $user, Entity $entity): bool
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->checkOwnershipTeam($user, $entity);
}
return false;
}
/**
* Check whether an entity belongs to a user account.
*/
public function checkOwnershipAccount(User $user, Entity $entity): bool
{
$checker = $this->getOwnershipChecker($entity->getEntityType());
if (!$checker instanceof OwnershipAccountChecker) {
return false;
}
return $checker->checkAccount($user, $entity);
}
/**
* Check whether an entity belongs to a user account.
*/
public function checkOwnershipContact(User $user, Entity $entity): bool
{
$checker = $this->getOwnershipChecker($entity->getEntityType());
if (!$checker instanceof OwnershipContactChecker) {
return false;
}
return $checker->checkContact($user, $entity);
}
public function checkScope(User $user, string $scope, ?string $action = null): bool
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->checkScope($user, $scope, $action);
}
return parent::checkScope($user, $scope, $action);
}
public function checkUser(User $user, string $permission, User $entity): bool
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->checkUser($user, $permission, $entity);
}
return parent::checkUser($user, $permission, $entity);
}
public function getScopeForbiddenAttributeList(
User $user,
string $scope,
string $action = PortalTable::ACTION_READ,
string $thresholdLevel = PortalTable::LEVEL_NO
): array {
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager
->getScopeForbiddenAttributeList($user, $scope, $action, $thresholdLevel);
}
return parent::getScopeForbiddenAttributeList($user, $scope, $action, $thresholdLevel);
}
public function getScopeForbiddenFieldList(
User $user,
string $scope,
string $action = Table::ACTION_READ,
string $thresholdLevel = Table::LEVEL_NO
): array {
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager
->getScopeForbiddenFieldList($user, $scope, $action, $thresholdLevel);
}
return parent::getScopeForbiddenFieldList($user, $scope, $action, $thresholdLevel);
}
protected function checkUserIsNotPortal(User $user): bool
{
return !$user->isPortal();
}
/**
* @deprecated As of v6.0. Use `getPermissionLevel` instead.
*/
public function get(User $user, string $permission): string
{
return $this->getPermissionLevel($user, $permission);
}
}

View File

@@ -0,0 +1,68 @@
<?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\Portal;
use Espo\Entities\Portal;
use Espo\Core\InjectableFactory;
use LogicException;
/**
* Used when logged to CRM (not to portal) to provide an access checking ability for a specific portal.
* E.g. check whether a portal user has access to some record within a specific portal.
*/
class AclManagerContainer
{
/**
* @var array<string, AclManager>
*/
private $data = [];
public function __construct(private InjectableFactory $injectableFactory)
{}
public function get(Portal $portal): AclManager
{
if (!$portal->hasId()) {
throw new LogicException("AclManagerContainer: portal should have ID.");
}
$id = $portal->getId();
if (!isset($this->data[$id])) {
$aclManager = $this->injectableFactory->create(AclManager::class);
$aclManager->setPortal($portal);
$this->data[$id] = $aclManager;
}
return $this->data[$id];
}
}

View 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\Portal\Api;
use Espo\Core\Api\MiddlewareProvider;
use Espo\Core\Api\Starter as StarterBase;
use Espo\Core\ApplicationState;
use Espo\Core\Portal\Utils\Route as RouteUtil;
use Espo\Core\Api\RouteProcessor;
use Espo\Core\Api\Route\RouteParamsFetcher;
use Espo\Core\Utils\Config\SystemConfig;
use Espo\Core\Utils\Log;
class Starter extends StarterBase
{
public function __construct(
RouteProcessor $requestProcessor,
RouteUtil $routeUtil,
RouteParamsFetcher $routeParamsFetcher,
MiddlewareProvider $middlewareProvider,
Log $log,
SystemConfig $systemConfig,
ApplicationState $applicationState
) {
$routeCacheFile = 'data/cache/application/slim-routes-portal-' . $applicationState->getPortalId() . '.php';
parent::__construct(
$requestProcessor,
$routeUtil,
$routeParamsFetcher,
$middlewareProvider,
$log,
$systemConfig,
$routeCacheFile
);
}
}

View File

@@ -0,0 +1,127 @@
<?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\Portal;
use Espo\Core\Application\ApplicationParams;
use Espo\Entities\Portal;
use Espo\ORM\EntityManager;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Application as BaseApplication;
use Espo\Core\Container\ContainerBuilder;
use Espo\Core\Portal\Container as PortalContainer;
use Espo\Core\Portal\Container\ContainerConfiguration as PortalContainerConfiguration;
use Espo\Core\Portal\Utils\Config;
use LogicException;
class Application extends BaseApplication
{
/**
* @throws Forbidden
* @throws NotFound
* @noinspection PhpMissingParentConstructorInspection
*/
public function __construct(
?string $portalId,
?ApplicationParams $params = null,
) {
date_default_timezone_set('UTC');
$this->initContainer($params);
$this->initPortal($portalId);
$this->initAutoloads();
$this->initPreloads();
}
protected function initContainer(?ApplicationParams $params): void
{
$container = (new ContainerBuilder())
->withConfigClassName(Config::class)
->withContainerClassName(PortalContainer::class)
->withContainerConfigurationClassName(PortalContainerConfiguration::class)
->withParams($params)
->build();
if (!$container instanceof PortalContainer) {
throw new LogicException("Wrong container created.");
}
$this->container = $container;
}
/**
* @throws Forbidden
* @throws NotFound
*/
protected function initPortal(?string $portalId): void
{
if (!$portalId) {
throw new LogicException("Portal ID was not passed to Portal\Application.");
}
$entityManager = $this->container->getByClass(EntityManager::class);
$portal = $entityManager->getEntityById(Portal::ENTITY_TYPE, $portalId);
if (!$portal) {
$portal = $entityManager
->getRDBRepositoryByClass(Portal::class)
->where(['customId' => $portalId])
->findOne();
}
if (!$portal) {
throw new NotFound("Portal $portalId not found.");
}
if (!$portal->isActive()) {
throw new Forbidden("Portal $portalId is not active.");
}
$container = $this->container;
if (!$container instanceof PortalContainer) {
throw new LogicException();
}
$container->setPortal($portal);
}
protected function initPreloads(): void
{
parent::initPreloads();
foreach ($this->getMetadata()->get(['app', 'portalContainerServices']) ?? [] as $name => $defs) {
if ($defs['preload'] ?? false) {
$this->container->get($name);
}
}
}
}

View 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\Portal\ApplicationRunners;
use Espo\Core\Application\Runner;
use Espo\Core\Portal\Api\Starter;
class Api implements Runner
{
public function __construct(private Starter $starter)
{}
public function run(): void
{
$this->starter->start();
}
}

View File

@@ -0,0 +1,57 @@
<?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\Portal\ApplicationRunners;
use Espo\Core\Application\Runner;
use Espo\Core\ApplicationState;
use Espo\Core\Utils\ClientManager;
/**
* Displays the main HTML page for a portal.
*/
class Client implements Runner
{
public function __construct(
private ClientManager $clientManager,
private ApplicationState $applicationState
) {}
public function run(): void
{
$portalId = $this->applicationState->getPortal()->getId();
$this->clientManager->display(null, null, [
'portalId' => $portalId,
'applicationId' => $portalId,
'apiUrl' => 'api/v1/portal-access/' . $portalId,
'appClientClassName' => 'app-portal',
]);
}
}

View File

@@ -0,0 +1,81 @@
<?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\Portal;
use Espo\Core\Container\Exceptions\NotSettableException;
use Espo\Entities\Portal as PortalEntity;
use Espo\Core\Portal\Utils\Config;
use Espo\Core\Container as BaseContainer;
use Psr\Container\NotFoundExceptionInterface;
use LogicException;
class Container extends BaseContainer
{
private const ID_PORTAL = 'portal';
private const ID_CONFIG = 'config';
private const ID_ACL_MANAGER = 'aclManager';
private bool $portalIsSet = false;
/**
* @throws NotSettableException
*/
public function setPortal(PortalEntity $portal): void
{
if ($this->portalIsSet) {
throw new NotSettableException("Can't set portal second time.");
}
$this->portalIsSet = true;
$this->setForced(self::ID_PORTAL, $portal);
$data = [];
foreach ($portal->getSettingsAttributeList() as $attribute) {
$data[$attribute] = $portal->get($attribute);
}
try {
/** @var Config $config */
$config = $this->get(self::ID_CONFIG);
$config->setPortalParameters($data);
/** @var AclManager $aclManager */
$aclManager = $this->get(self::ID_ACL_MANAGER);
} catch (NotFoundExceptionInterface) {
throw new LogicException();
}
$aclManager->setPortal($portal);
}
}

View File

@@ -0,0 +1,88 @@
<?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\Portal\Container;
use Espo\Core\Container\ContainerConfiguration as BaseContainerConfiguration;
class ContainerConfiguration extends BaseContainerConfiguration
{
/**
* @return ?class-string
*/
public function getLoaderClassName(string $name): ?string
{
$className = null;
try {
$className = $this->metadata->get(['app', 'portalContainerServices', $name, 'loaderClassName']);
} catch (\Exception) {}
if ($className && class_exists($className)) {
return $className;
}
$className = 'Espo\Custom\Core\Portal\Loaders\\' . ucfirst($name);
if (!class_exists($className)) {
$className = 'Espo\Core\Portal\Loaders\\' . ucfirst($name);
}
if (class_exists($className)) {
return $className;
}
return parent::getLoaderClassName($name);
}
/**
* @return ?class-string
*/
public function getServiceClassName(string $name): ?string
{
return $this->metadata->get(['app', 'portalContainerServices', $name, 'className']) ??
parent::getServiceClassName($name);
}
/**
* @return ?string[]
*/
public function getServiceDependencyList(string $name): ?array
{
return
$this->metadata->get(['app', 'portalContainerServices', $name, 'dependencyList']) ??
parent::getServiceDependencyList($name);
}
public function isSettable(string $name): bool
{
return
$this->metadata->get(['app', 'portalContainerServices', $name, 'settable']) ??
parent::isSettable($name);
}
}

View 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\Portal\Loaders;
use Espo\Core\Portal\AclManager as PortalAclManager;
use Espo\Core\AclManager;
use Espo\Core\Container\Loader;
use Espo\Core\Portal\Acl as AclService;
use Espo\Entities\User;
use InvalidArgumentException;
class Acl implements Loader
{
private PortalAclManager $aclManager;
private User $user;
public function __construct(AclManager $aclManager, User $user)
{
if (!$aclManager instanceof PortalAclManager) {
throw new InvalidArgumentException();
}
$this->aclManager = $aclManager;
$this->user = $user;
}
public function load(): AclService
{
return new AclService($this->aclManager, $this->user);
}
}

View File

@@ -0,0 +1,50 @@
<?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\Portal\Loaders;
use Espo\Core\AclManager as InternalAclManager;
use Espo\Core\Container\Loader;
use Espo\Core\InjectableFactory;
use Espo\Core\Portal\AclManager as PortalAclManager;
class AclManager implements Loader
{
public function __construct(
private InjectableFactory $injectableFactory,
private InternalAclManager $internalAclManager
) {}
public function load(): PortalAclManager
{
return $this->injectableFactory->createWith(PortalAclManager::class,[
'internalAclManager' => $this->internalAclManager,
]);
}
}

View 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\Portal\Loaders;
use Espo\Core\AclManager as InternalAclManagerService;
use Espo\Core\Container\Loader;
use Espo\Core\InjectableFactory;
class InternalAclManager implements Loader
{
public function __construct(private InjectableFactory $injectableFactory)
{}
public function load(): InternalAclManagerService
{
return $this->injectableFactory->create(InternalAclManagerService::class);
}
}

View File

@@ -0,0 +1,54 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Portal\Loaders;
use Espo\Core\Container\Loader;
use Espo\Core\InjectableFactory;
use Espo\Core\Portal\Utils\Language as LanguageService;
use Espo\Core\Utils\Config;
use Espo\Entities\Preferences;
class Language implements Loader
{
public function __construct(
private InjectableFactory $injectableFactory,
private Config $config,
private Preferences $preferences
) {}
public function load(): LanguageService
{
return $this->injectableFactory->createWith(LanguageService::class, [
'language' => LanguageService::detectLanguage($this->config, $this->preferences),
'useCache' => $this->config->get('useCache') ?? false,
]);
}
}

View File

@@ -0,0 +1,160 @@
<?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\Portal\Utils;
use Espo\Core\Utils\Config as BaseConfig;
use RuntimeException;
use stdClass;
class Config extends BaseConfig
{
private bool $portalParamsSet = false;
/**
* @var array<string, mixed>
*/
private $portalData = [];
/**
* @var string[]
*/
private $portalParamList = [
'applicationName',
'companyLogoId',
'tabList',
'quickCreateList',
'dashboardLayout',
'dashletsOptions',
'theme',
'themeParams',
'language',
'timeZone',
'dateFormat',
'timeFormat',
'weekStart',
'defaultCurrency',
];
/**
* @param mixed $default
* @return mixed
*/
public function get(string $name, $default = null)
{
if (array_key_exists($name, $this->portalData)) {
return $this->portalData[$name];
}
return parent::get($name, $default);
}
public function has(string $name): bool
{
if (array_key_exists($name, $this->portalData)) {
return true;
}
return parent::has($name);
}
public function getAllNonInternalData(): stdClass
{
$data = parent::getAllNonInternalData();
foreach ($this->portalData as $k => $v) {
$data->$k = $v;
}
return $data;
}
/**
* Override parameters for a portal. Can be called only once.
*
* @param array<string, mixed> $data
*/
public function setPortalParameters(array $data = []): void
{
if ($this->portalParamsSet) {
throw new RuntimeException("Can't set portal params second time.");
}
$this->portalParamsSet = true;
if (empty($data['applicationName'])) {
unset($data['applicationName']);
}
if (empty($data['language'])) {
unset($data['language']);
}
if (empty($data['theme'])) {
unset($data['theme']);
}
if (empty($data['timeZone'])) {
unset($data['timeZone']);
}
if (empty($data['dateFormat'])) {
unset($data['dateFormat']);
}
if (empty($data['timeFormat'])) {
unset($data['timeFormat']);
}
if (empty($data['defaultCurrency'])) {
unset($data['defaultCurrency']);
}
if (isset($data['weekStart']) && $data['weekStart'] === -1) {
unset($data['weekStart']);
}
if (array_key_exists('weekStart', $data) && is_null($data['weekStart'])) {
unset($data['weekStart']);
}
if ($this->get('webSocketInPortalDisabled')) {
$this->portalData['useWebSocket'] = false;
}
foreach ($data as $attribute => $value) {
if (!in_array($attribute, $this->portalParamList)) {
continue;
}
$this->portalData[$attribute] = $value;
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Portal\Utils;
class Language extends \Espo\Core\Utils\Language {}

View 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\Portal\Utils;
use Espo\Core\Api\Route as RouteItem;
use Espo\Core\Utils\Route as BaseRoute;
class Route extends BaseRoute
{
public function getFullList(): array
{
$originalRouteList = parent::getFullList();
$newRouteList = [];
foreach ($originalRouteList as $route) {
$path = $route->getAdjustedRoute();
if ($path[0] !== '/') {
$path = '/' . $path;
}
$path = '/{portalId}' . $path;
$newRoute = new RouteItem(
$route->getMethod(),
$route->getRoute(),
$path,
$route->getParams(),
$route->noAuth(),
$route->getActionClassName()
);
$newRouteList[] = $newRoute;
}
return $newRouteList;
}
}

View File

@@ -0,0 +1,63 @@
<?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\Portal\Utils;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Metadata;
use Espo\Core\Utils\Theme\MetadataProvider;
use Espo\Entities\Portal;
use Espo\Core\Utils\ThemeManager as BaseThemeManager;
class ThemeManager extends BaseThemeManager
{
private Portal $portal;
public function __construct(
Config $config,
Metadata $metadata,
MetadataProvider $metadataProvider,
Portal $portal,
) {
parent::__construct($config, $metadata, $metadataProvider);
$this->portal = $portal;
}
public function getName(): string
{
$theme = $this->portal->get('theme');
if ($theme) {
return $theme;
}
return parent::getName();
}
}

View File

@@ -0,0 +1,160 @@
<?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\Portal\Utils;
class Url
{
public static function detectPortalIdForApi(): ?string
{
$portalId = filter_input(INPUT_GET, 'portalId');
if ($portalId) {
return $portalId;
}
$url = $_SERVER['REQUEST_URI'] ?? null;
$scriptName = $_SERVER['SCRIPT_NAME'];
if (!$url) {
return null;
}
$scriptNameModified = str_replace('public/api/', 'api/', $scriptName);
return explode('/', $url)[count(explode('/', $scriptNameModified)) - 1] ?? null;
}
public static function getPortalIdFromEnv(): ?string
{
return $_SERVER['ESPO_PORTAL_ID'] ?? null;
}
public static function detectPortalId(): ?string
{
$portalId = self::getPortalIdFromEnv();
if ($portalId) {
return $portalId;
}
$url = $_SERVER['REQUEST_URI'] ?? null;
$scriptName = $_SERVER['SCRIPT_NAME'];
$scriptNameModified = str_replace('public/api/', 'api/', $scriptName);
$idIndex = count(explode('/', $scriptNameModified)) - 1;
if ($url) {
$portalId = explode('/', $url)[$idIndex] ?? null;
if (str_contains($url, '=')) {
$portalId = null;
}
}
if ($portalId) {
return $portalId;
}
$url = $_SERVER['REDIRECT_URL'] ?? null;
if (!$url) {
return null;
}
$portalId = explode('/', $url)[$idIndex] ?? null;
if ($portalId === '') {
$portalId = null;
}
return $portalId;
}
protected static function detectIsCustomUrl(): bool
{
return (bool) ($_SERVER['ESPO_PORTAL_IS_CUSTOM_URL'] ?? false);
}
public static function detectIsInPortalDir(): bool
{
$isCustomUrl = self::detectIsCustomUrl();
if ($isCustomUrl) {
return false;
}
$a = explode('?', $_SERVER['REQUEST_URI']);
$url = rtrim($a[0], '/');
return str_contains($url, '/portal');
}
public static function detectIsInPortalWithId(): bool
{
if (!self::detectIsInPortalDir()) {
return false;
}
$url = $_SERVER['REQUEST_URI'];
$a = explode('?', $url);
$url = rtrim($a[0], '/');
$folders = explode('/', $url);
if (count($folders) > 1 && $folders[count($folders) - 2] === 'portal') {
return true;
}
return false;
}
public static function getRedirectUrlWithTrailingSlash(): ?string
{
$uri = $_SERVER['REQUEST_URI'];
if ($uri === '' || $uri === '/' || str_ends_with($uri, '/')) {
return null;
}
$output = $uri . '/';
$queryString = $_SERVER['QUERY_STRING'] ?? null;
if ($queryString !== null && $queryString !== '') {
$output .= '?' . $_SERVER['QUERY_STRING'];
}
return $output;
}
}