lockable
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Record\Common\Lock;
|
||||
|
||||
use Espo\Core\Record\DeleteParams;
|
||||
use Espo\Core\Record\Hook\DeleteHook;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Tools\Lock\LockValidationHelper;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class BeforeDeleteValidation implements DeleteHook
|
||||
{
|
||||
public function __construct(
|
||||
private LockValidationHelper $lockValidationHelper,
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity, DeleteParams $params): void
|
||||
{
|
||||
$this->lockValidationHelper->validateBeforeRemove($entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Record\Common\Lock;
|
||||
|
||||
use Espo\Core\Record\Hook\SaveHook;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Tools\Lock\LockValidationHelper;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class BeforeSaveValidation implements SaveHook
|
||||
{
|
||||
public function __construct(
|
||||
private LockValidationHelper $lockValidationHelper,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function process(Entity $entity): void
|
||||
{
|
||||
$this->lockValidationHelper->validateBeforeSave($entity);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Defs\Params\FieldParam;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
@@ -333,6 +334,7 @@ class Helper
|
||||
}
|
||||
|
||||
return $fieldDefs->getType() === FieldType::LINK &&
|
||||
!$fieldDefs->getParam(FieldParam::DISABLED) &&
|
||||
$entityDefs->hasRelation(Field::ASSIGNED_USER) &&
|
||||
$entityDefs->getRelation(Field::ASSIGNED_USER)->getForeignEntityType() === User::ENTITY_TYPE;
|
||||
}
|
||||
|
||||
@@ -43,4 +43,5 @@ class Permission
|
||||
public const USER_CALENDAR = 'userCalendar';
|
||||
public const FOLLOWER_MANAGEMENT = 'followerManagement';
|
||||
public const GROUP_EMAIL_ACCOUNT = 'groupEmailAccount';
|
||||
public const LOCK = 'lock';
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Core\Action;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
@@ -39,6 +40,7 @@ interface Action
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
* @throws NotFound
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function process(Params $params, Data $data): void;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Action\Actions;
|
||||
|
||||
use Espo\Core\Action\Action;
|
||||
use Espo\Core\Action\Data;
|
||||
use Espo\Core\Action\Params;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Record\EntityProvider;
|
||||
use Espo\Tools\Lock\LockService;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class Lock implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private LockService $lockService,
|
||||
private EntityProvider $entityProvider,
|
||||
) {}
|
||||
|
||||
public function process(Params $params, Data $data): void
|
||||
{
|
||||
$entityType = $params->getEntityType();
|
||||
$id = $params->getId();
|
||||
|
||||
if (!$this->lockService->isEnabled($entityType)) {
|
||||
throw new ForbiddenSilent("Not enabled.");
|
||||
}
|
||||
|
||||
if (!$this->lockService->isAllowed($entityType)) {
|
||||
throw new ForbiddenSilent("Not allowed.");
|
||||
}
|
||||
|
||||
$entity = $this->entityProvider->get($entityType, $id);
|
||||
|
||||
try {
|
||||
$this->lockService->lock($entity);
|
||||
} catch (Error $e) {
|
||||
throw new Forbidden(previous: $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,14 +32,17 @@ namespace Espo\Core\Action\Actions\Merge;
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Action\Params;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Name\Link;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Record\ActionHistory\Action;
|
||||
use Espo\Core\Record\DeleteParams;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Record\UpdateParams;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\ObjectUtil;
|
||||
use Espo\Entities\Attachment;
|
||||
@@ -47,7 +50,6 @@ use Espo\Entities\Note;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\EmailAddress;
|
||||
use Espo\Entities\PhoneNumber;
|
||||
|
||||
use Espo\ORM\Type\RelationType;
|
||||
use stdClass;
|
||||
|
||||
@@ -64,6 +66,8 @@ class Merger
|
||||
* @param string[] $sourceIdList
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
* @throws BadRequest
|
||||
* @noinspection PhpDocRedundantThrowsInspection
|
||||
*/
|
||||
public function process(Params $params, array $sourceIdList, stdClass $data): void
|
||||
@@ -77,6 +81,8 @@ class Merger
|
||||
* @param string[] $sourceIdList
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function processInternal(Params $params, array $sourceIdList, stdClass $data): void
|
||||
{
|
||||
@@ -145,18 +151,10 @@ class Merger
|
||||
$this->updateNotes($sourceEntity, $entity);
|
||||
}
|
||||
|
||||
$mergeLinkList = $this->getMergeLinkList($entityType);
|
||||
$this->mergeLinks($entity, $sourceEntityList);
|
||||
|
||||
foreach ($sourceEntityList as $sourceEntity) {
|
||||
foreach ($mergeLinkList as $link) {
|
||||
$this->updateRelations($sourceEntity, $entity, $link);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($sourceEntityList as $sourceEntity) {
|
||||
$this->entityManager->removeEntity($sourceEntity);
|
||||
|
||||
$service->processActionHistoryRecord(Action::DELETE, $sourceEntity);
|
||||
$service->delete($sourceEntity->getId(), DeleteParams::create());
|
||||
}
|
||||
|
||||
if ($hasPhoneNumber) {
|
||||
@@ -167,11 +165,7 @@ class Merger
|
||||
$this->prepareEmailAddressData($emailAddressToRelateList, $clonedData);
|
||||
}
|
||||
|
||||
$entity->set($clonedData);
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
|
||||
$service->processActionHistoryRecord(Action::UPDATE, $entity);
|
||||
$service->update($id, $clonedData, UpdateParams::create()->withSkipDuplicateCheck());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -449,4 +443,18 @@ class Merger
|
||||
|
||||
return $columnAttributeMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Entity[] $sourceEntityList
|
||||
*/
|
||||
private function mergeLinks(Entity $entity, array $sourceEntityList): void
|
||||
{
|
||||
$mergeLinkList = $this->getMergeLinkList($entity->getEntityType());
|
||||
|
||||
foreach ($sourceEntityList as $sourceEntity) {
|
||||
foreach ($mergeLinkList as $link) {
|
||||
$this->updateRelations($sourceEntity, $entity, $link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Action\Actions;
|
||||
|
||||
use Espo\Core\Action\Action;
|
||||
use Espo\Core\Action\Data;
|
||||
use Espo\Core\Action\Params;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Record\EntityProvider;
|
||||
use Espo\Tools\Lock\LockService;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class Unlock implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private LockService $lockService,
|
||||
private EntityProvider $entityProvider,
|
||||
) {}
|
||||
|
||||
public function process(Params $params, Data $data): void
|
||||
{
|
||||
$entityType = $params->getEntityType();
|
||||
$id = $params->getId();
|
||||
|
||||
if (!$this->lockService->isEnabled($entityType)) {
|
||||
throw new ForbiddenSilent("Not enabled.");
|
||||
}
|
||||
|
||||
if (!$this->lockService->isAllowed($entityType)) {
|
||||
throw new ForbiddenSilent("Not allowed.");
|
||||
}
|
||||
|
||||
$entity = $this->entityProvider->get($entityType, $id);
|
||||
|
||||
try {
|
||||
$this->lockService->unlock($entity);
|
||||
} catch (Error $e) {
|
||||
throw new Forbidden(previous: $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ namespace Espo\Core\Action;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
@@ -55,6 +56,7 @@ class Service
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
* @throws NotFound
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function process(string $entityType, string $action, string $id, stdClass $data): Entity
|
||||
{
|
||||
|
||||
@@ -39,6 +39,7 @@ use Espo\Core\Exceptions\HasLogMessage;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Utils\Log;
|
||||
|
||||
use Espo\ORM\Exceptions\ValidationException;
|
||||
use LogicException;
|
||||
use Psr\Log\LogLevel;
|
||||
use RuntimeException;
|
||||
@@ -82,6 +83,11 @@ class ErrorOutput
|
||||
NotFound::class,
|
||||
];
|
||||
|
||||
/** @var array<class-string<Throwable>, int> */
|
||||
private array $statusCodeMap = [
|
||||
ValidationException::class => 409,
|
||||
];
|
||||
|
||||
public function __construct(private Log $log)
|
||||
{}
|
||||
|
||||
@@ -136,6 +142,8 @@ class ErrorOutput
|
||||
'request' => $request,
|
||||
]);
|
||||
|
||||
$statusCode = $this->statusCodeMap[$exception::class] ?? $statusCode;
|
||||
|
||||
if (!in_array($statusCode, $this->allowedStatusCodeList)) {
|
||||
$statusCode = 500;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\MassAction\Actions;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\ErrorSilent;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\MassAction\Data;
|
||||
use Espo\Core\MassAction\MassAction;
|
||||
use Espo\Core\MassAction\Params;
|
||||
use Espo\Core\MassAction\QueryBuilder;
|
||||
use Espo\Core\MassAction\Result;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\Lock\LockService;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class MassLock implements MassAction
|
||||
{
|
||||
public function __construct(
|
||||
private QueryBuilder $queryBuilder,
|
||||
private LockService $lockService,
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl,
|
||||
) {}
|
||||
|
||||
public function process(Params $params, Data $data): Result
|
||||
{
|
||||
$entityType = $params->getEntityType();
|
||||
|
||||
if (!$this->lockService->isEnabled($entityType)) {
|
||||
throw new ErrorSilent("Not enabled.");
|
||||
}
|
||||
|
||||
if (!$this->lockService->isAllowed($entityType)) {
|
||||
throw new ForbiddenSilent("Not allowed.");
|
||||
}
|
||||
|
||||
if ($this->acl->getPermissionLevel(Acl\Permission::MASS_UPDATE) !== Acl\Table::LEVEL_YES) {
|
||||
throw new ForbiddenSilent("No mass update permission.");
|
||||
}
|
||||
|
||||
$query = $this->queryBuilder->build($params);
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository($entityType)
|
||||
->clone($query)
|
||||
->sth()
|
||||
->find();
|
||||
|
||||
return $this->lockService->massLock($collection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\MassAction\Actions;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\ErrorSilent;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\MassAction\Data;
|
||||
use Espo\Core\MassAction\MassAction;
|
||||
use Espo\Core\MassAction\Params;
|
||||
use Espo\Core\MassAction\QueryBuilder;
|
||||
use Espo\Core\MassAction\Result;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\Lock\LockService;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class MassUnlock implements MassAction
|
||||
{
|
||||
public function __construct(
|
||||
private QueryBuilder $queryBuilder,
|
||||
private LockService $lockService,
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl,
|
||||
) {}
|
||||
|
||||
public function process(Params $params, Data $data): Result
|
||||
{
|
||||
$entityType = $params->getEntityType();
|
||||
|
||||
if (!$this->lockService->isEnabled($entityType)) {
|
||||
throw new ErrorSilent("Not enabled.");
|
||||
}
|
||||
|
||||
if (!$this->lockService->isAllowed($entityType)) {
|
||||
throw new ForbiddenSilent("Not allowed.");
|
||||
}
|
||||
|
||||
if ($this->acl->getPermissionLevel(Acl\Permission::MASS_UPDATE) !== Acl\Table::LEVEL_YES) {
|
||||
throw new ForbiddenSilent("No mass update permission.");
|
||||
}
|
||||
|
||||
$query = $this->queryBuilder->build($params);
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository($entityType)
|
||||
->clone($query)
|
||||
->sth()
|
||||
->find();
|
||||
|
||||
return $this->lockService->massUnlock($collection);
|
||||
}
|
||||
}
|
||||
@@ -49,4 +49,5 @@ class Field
|
||||
public const EMAIL_ADDRESS = 'emailAddress';
|
||||
public const PHONE_NUMBER = 'phoneNumber';
|
||||
public const VERSION_NUMBER = 'versionNumber';
|
||||
public const string IS_LOCKED = 'isLocked';
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ use Espo\ORM\Entity;
|
||||
use Espo\Core\Record\CreateParams;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
* @template TEntity of Entity = Entity
|
||||
*/
|
||||
interface CreateHook
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@ use Espo\ORM\Entity;
|
||||
use Espo\Core\Record\DeleteParams;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
* @template TEntity of Entity = Entity
|
||||
*/
|
||||
interface DeleteHook
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Espo\Core\Record\Hook;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
* @template TEntity of Entity = Entity
|
||||
*/
|
||||
interface LinkHook
|
||||
{
|
||||
|
||||
@@ -97,7 +97,10 @@ class Provider
|
||||
$key = $type . 'HookClassNameList';
|
||||
|
||||
/** @var class-string[] $classNameList */
|
||||
$classNameList = $this->metadata->get(['recordDefs', $entityType, $key]) ?? [];
|
||||
$classNameList = [
|
||||
...$this->metadata->get("app.record.$key", []),
|
||||
...$this->metadata->get("recordDefs.$entityType.$key", [])
|
||||
];
|
||||
|
||||
$interfaces = $this->typeInterfaceListMap[$type] ?? null;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ use Espo\ORM\Entity;
|
||||
use Espo\Core\Record\ReadParams;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
* @template TEntity of Entity = Entity
|
||||
*/
|
||||
interface ReadHook
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ use Espo\ORM\Entity;
|
||||
/**
|
||||
* On record create or update.
|
||||
*
|
||||
* @template TEntity of Entity
|
||||
* @template TEntity of Entity = Entity
|
||||
* @since 8.1.0.
|
||||
*/
|
||||
interface SaveHook
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Espo\Core\Record\Hook;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
* @template TEntity of Entity = Entity
|
||||
*/
|
||||
interface UnlinkHook
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ use Espo\ORM\Entity;
|
||||
use Espo\Core\Record\UpdateParams;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
* @template TEntity of Entity = Entity
|
||||
*/
|
||||
interface UpdateHook
|
||||
{
|
||||
|
||||
@@ -95,7 +95,10 @@ class FilterProvider
|
||||
private function getCreateClassNameList(string $entityType): array
|
||||
{
|
||||
/** @var class-string<Filter>[] */
|
||||
return $this->metadata->get("recordDefs.$entityType.createInputFilterClassNameList") ?? [];
|
||||
return [
|
||||
...$this->metadata->get("app.record.createInputFilterClassNameList", []),
|
||||
...$this->metadata->get("recordDefs.$entityType.createInputFilterClassNameList", [])
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,6 +107,9 @@ class FilterProvider
|
||||
private function getUpdateClassNameList(string $entityType): array
|
||||
{
|
||||
/** @var class-string<Filter>[] */
|
||||
return $this->metadata->get("recordDefs.$entityType.updateInputFilterClassNameList") ?? [];
|
||||
return [
|
||||
...$this->metadata->get("app.record.updateInputFilterClassNameList", []),
|
||||
...$this->metadata->get("recordDefs.$entityType.updateInputFilterClassNameList", [])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,9 @@ class FilterProvider
|
||||
private function getClassNameList(string $entityType): array
|
||||
{
|
||||
/** @var class-string<Filter<Entity>>[] */
|
||||
return $this->metadata->get("recordDefs.$entityType.outputFilterClassNameList") ?? [];
|
||||
return [
|
||||
...$this->metadata->get("app.record.outputFilterClassNameList", []),
|
||||
...$this->metadata->get("recordDefs.$entityType.outputFilterClassNameList", []),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,7 +580,7 @@ class Service implements Crud,
|
||||
->getFieldList();
|
||||
|
||||
foreach ($fieldDefsList as $fieldDefs) {
|
||||
if (!$fieldDefs->getParam('readOnlyAfterCreate')) {
|
||||
if (!$fieldDefs->getParam(FieldParam::READ_ONLY_AFTER_CREATE)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,25 @@ class Builder
|
||||
['app', 'client', 'scriptList'],
|
||||
['app', 'client', 'cssList'],
|
||||
['app', 'client', 'linkList'],
|
||||
|
||||
['app', 'record', 'selectApplierClassNameList'],
|
||||
['app', 'record', 'createInputFilterClassNameList'],
|
||||
['app', 'record', 'updateInputFilterClassNameList'],
|
||||
['app', 'record', 'outputFilterClassNameList'],
|
||||
['app', 'record', 'beforeReadHookClassNameList'],
|
||||
['app', 'record', 'earlyBeforeCreateHookClassNameList'],
|
||||
['app', 'record', 'beforeCreateHookClassNameList'],
|
||||
['app', 'record', 'earlyBeforeUpdateHookClassNameList'],
|
||||
['app', 'record', 'beforeUpdateHookClassNameList'],
|
||||
['app', 'record', 'beforeDeleteHookClassNameList'],
|
||||
['app', 'record', 'afterCreateHookClassNameList'],
|
||||
['app', 'record', 'afterUpdateHookClassNameList'],
|
||||
['app', 'record', 'afterDeleteHookClassNameList'],
|
||||
['app', 'record', 'beforeLinkHookClassNameList'],
|
||||
['app', 'record', 'beforeUnlinkHookClassNameList'],
|
||||
['app', 'record', 'afterLinkHookClassNameList'],
|
||||
['app', 'record', 'afterUnlinkHookClassNameList'],
|
||||
|
||||
['recordDefs', self::ANY_KEY, 'readLoaderClassNameList'],
|
||||
['recordDefs', self::ANY_KEY, 'listLoaderClassNameList'],
|
||||
['recordDefs', self::ANY_KEY, 'saverClassNameList'],
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Hooks\Common;
|
||||
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Hook\Hook\BeforeRemove;
|
||||
use Espo\Core\Hook\Hook\BeforeSave;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Exceptions\ValidationException;
|
||||
use Espo\ORM\Repository\Option\RemoveOptions;
|
||||
use Espo\ORM\Repository\Option\SaveOptions;
|
||||
use Espo\Tools\Lock\LockValidationHelper;
|
||||
use Espo\Tools\Lock\LockMetadataProvider;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class ValidateLocked implements BeforeSave, BeforeRemove
|
||||
{
|
||||
public static int $order = 12;
|
||||
|
||||
public function __construct(
|
||||
private LockMetadataProvider $lockMetadataProvider,
|
||||
private LockValidationHelper $lockValidationHelper,
|
||||
) {}
|
||||
|
||||
public function beforeSave(Entity $entity, SaveOptions $options): void
|
||||
{
|
||||
if (!$this->lockMetadataProvider->isEnabled($entity->getEntityType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->lockValidationHelper->validateBeforeSave($entity);
|
||||
} catch (Conflict $e) {
|
||||
throw new ValidationException(previous: $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function beforeRemove(Entity $entity, RemoveOptions $options): void
|
||||
{
|
||||
if (!$this->lockMetadataProvider->isEnabled($entity->getEntityType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->lockValidationHelper->validateBeforeRemove($entity);
|
||||
} catch (Conflict $e) {
|
||||
throw new ValidationException(previous: $e);
|
||||
}
|
||||
|
||||
if ($entity->get(Field::IS_LOCKED)) {
|
||||
throw new ValidationException("Cannot remove a locked record.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,13 @@
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Contact": {
|
||||
@@ -59,6 +66,13 @@
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Lead": {
|
||||
@@ -90,6 +104,13 @@
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Opportunity": {
|
||||
@@ -106,6 +127,13 @@
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Document": {
|
||||
@@ -122,6 +150,13 @@
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Case": {
|
||||
@@ -138,6 +173,13 @@
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"KnowledgeBaseArticle": {
|
||||
@@ -154,6 +196,13 @@
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Meeting": {
|
||||
@@ -192,6 +241,13 @@
|
||||
"tooltip": true,
|
||||
"view": "crm:views/admin/entity-manager/fields/status-list"
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Call": {
|
||||
@@ -230,6 +286,13 @@
|
||||
"tooltip": true,
|
||||
"view": "crm:views/admin/entity-manager/fields/status-list"
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Task": {
|
||||
@@ -264,6 +327,13 @@
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"TargetList": {
|
||||
@@ -273,6 +343,13 @@
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@Event": {
|
||||
@@ -343,6 +420,13 @@
|
||||
"tooltip": true,
|
||||
"view": "views/admin/entity-manager/fields/acl-account-link"
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +160,15 @@
|
||||
"campaign": {
|
||||
"type": "link"
|
||||
},
|
||||
"isLocked": {
|
||||
"type": "bool",
|
||||
"readOnly": true,
|
||||
"audited": true,
|
||||
"layoutAvailabilityList": [
|
||||
"filters",
|
||||
"list"
|
||||
]
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "datetime",
|
||||
"readOnly": true,
|
||||
@@ -188,11 +197,13 @@
|
||||
},
|
||||
"assignedUser": {
|
||||
"type": "link",
|
||||
"view": "views/fields/assigned-user"
|
||||
"view": "views/fields/assigned-user",
|
||||
"notLockable": true
|
||||
},
|
||||
"teams": {
|
||||
"type": "linkMultiple",
|
||||
"view": "views/fields/teams"
|
||||
"view": "views/fields/teams",
|
||||
"notLockable": true
|
||||
},
|
||||
"targetLists": {
|
||||
"type": "linkMultiple",
|
||||
|
||||
@@ -12,5 +12,6 @@
|
||||
"notifications": true,
|
||||
"object": true,
|
||||
"hasPersonalData": true,
|
||||
"duplicateCheckFieldList": ["name", "emailAddress"]
|
||||
"duplicateCheckFieldList": ["name", "emailAddress"],
|
||||
"lockable": true
|
||||
}
|
||||
|
||||
@@ -74,6 +74,13 @@ class FieldParam
|
||||
*/
|
||||
public const READ_ONLY = 'readOnly';
|
||||
|
||||
/**
|
||||
* Read-only after create.
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
public const READ_ONLY_AFTER_CREATE = 'readOnlyAfterCreate';
|
||||
|
||||
/**
|
||||
* Decimal.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\ORM\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* A low level validation error.
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
class ValidationException extends RuntimeException
|
||||
{}
|
||||
@@ -243,7 +243,8 @@
|
||||
"onlyAdmin": "Only for Admin",
|
||||
"notStorable": "Not Storable",
|
||||
"itemsEditable": "Items Editable",
|
||||
"openInNewTab": "Open in new tab"
|
||||
"openInNewTab": "Open in new tab",
|
||||
"notLockable": "Not Lockable"
|
||||
},
|
||||
"strings" : {
|
||||
"rebuildRequired": "Rebuild is required"
|
||||
|
||||
@@ -56,7 +56,8 @@
|
||||
"collaborators": "Collaborators",
|
||||
"aclContactLink": "ACL Contact Link",
|
||||
"aclAccountLink": "ACL Account Link",
|
||||
"categories": "Categories"
|
||||
"categories": "Categories",
|
||||
"lockable": "Lockable"
|
||||
},
|
||||
"options": {
|
||||
"type": {
|
||||
@@ -99,6 +100,7 @@
|
||||
"beforeSaveApiScript": "A script called on create and update API requests before an entity is saved. Use for custom validation and duplicate checking."
|
||||
},
|
||||
"tooltips": {
|
||||
"lockable": "Enables the ability to lock records.",
|
||||
"categories": "Enable the category tree feature. Records can be placed into categories.",
|
||||
"aclContactLink": "The link with Contact to use when applying access control for portal users.",
|
||||
"aclAccountLink": "The link with Account to use when applying access control for portal users.",
|
||||
|
||||
@@ -165,6 +165,8 @@
|
||||
"Linked": "Linked",
|
||||
"Unlinked": "Unlinked",
|
||||
"Done": "Done",
|
||||
"Bad request": "Bad request",
|
||||
"Conflict": "Conflict",
|
||||
"Access denied": "Access denied",
|
||||
"Not found": "Not found",
|
||||
"Internal server error": "Internal server error",
|
||||
@@ -334,7 +336,9 @@
|
||||
"View User Access": "View User Access",
|
||||
"Reacted": "Reacted",
|
||||
"Reaction Removed": "Reaction Removed",
|
||||
"Reactions": "Reactions"
|
||||
"Reactions": "Reactions",
|
||||
"Lock": "Lock",
|
||||
"Unlock": "Unlock"
|
||||
},
|
||||
"messages": {
|
||||
"checkLogsForDetails": "Check logs for details.",
|
||||
@@ -452,7 +456,15 @@
|
||||
"duplicateConflict": "A record already exists.",
|
||||
"cannotRemoveCategoryWithChildCategory": "Cannot remove a category that has a child category.",
|
||||
"cannotRemoveNotEmptyCategory": "Cannot remove a non-empty category.",
|
||||
"sameRecordIsAlreadyBeingEdited": "The record is already being edited."
|
||||
"sameRecordIsAlreadyBeingEdited": "The record is already being edited.",
|
||||
"confirmMassLock": "Are you sure you want to lock selected records?",
|
||||
"confirmMassUnlock": "Are you sure you want to unlock selected records?",
|
||||
"locked": "Record is locked",
|
||||
"unlocked": "Record is unlocked",
|
||||
"massLockDone": "{count} locked",
|
||||
"massUnlockDone": "{count} unlocked",
|
||||
"cannotModifyLockedRecord": "Cannot modify the field *{field}*. The record is locked.",
|
||||
"cannotRemoveLockedRecord": "Cannot remove a locked record."
|
||||
},
|
||||
"boolFilters": {
|
||||
"onlyMy": "Only My",
|
||||
@@ -478,7 +490,9 @@
|
||||
"unfollow": "Unfollow",
|
||||
"convertCurrency": "Convert Currency",
|
||||
"recalculateFormula": "Recalculate Formula",
|
||||
"printPdf": "Print to PDF"
|
||||
"printPdf": "Print to PDF",
|
||||
"lock": "Lock",
|
||||
"unlock": "Unlock"
|
||||
},
|
||||
"fields": {
|
||||
"name": "Name",
|
||||
@@ -522,7 +536,8 @@
|
||||
"types": "Types",
|
||||
"targetListIsOptedOut": "Is Opted Out (Target List)",
|
||||
"childList": "Child List",
|
||||
"category": "Category"
|
||||
"category": "Category",
|
||||
"isLocked": "Locked"
|
||||
},
|
||||
"links": {
|
||||
"assignedUser": "Assigned User",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"auditPermission": "Audit Permission",
|
||||
"mentionPermission": "Mention Permission",
|
||||
"userCalendarPermission": "User Calendar Permission",
|
||||
"lockPermission": "Lock Permission",
|
||||
"data": "Data",
|
||||
"fieldData": "Field Data"
|
||||
},
|
||||
@@ -22,6 +23,7 @@
|
||||
"teams": "Teams"
|
||||
},
|
||||
"tooltips": {
|
||||
"lockPermission": "Allows to lock and unlock records.",
|
||||
"messagePermission": "Allows to send messages to other users.\n\n* all – can send to all\n* team – can send only to teammates\n* no – cannot send",
|
||||
"assignmentPermission": "Allows to assign records to other users.\n\n* all – no restriction\n* team – can assign only to teammates\n* no – can assign only to self",
|
||||
"userPermission": "Allows to view stream of other users. Allows users to view the access levels other users have for specific records.",
|
||||
|
||||
@@ -29,6 +29,11 @@
|
||||
{"name": "auditPermission"},
|
||||
{"name": "userPermission"},
|
||||
{"name": "dataPrivacyPermission"}
|
||||
],
|
||||
[
|
||||
{"name": "lockPermission"},
|
||||
false,
|
||||
false
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -176,7 +176,8 @@
|
||||
"portalPermission",
|
||||
"groupEmailAccountPermission",
|
||||
"followerManagementPermission",
|
||||
"dataPrivacyPermission"
|
||||
"dataPrivacyPermission",
|
||||
"lockPermission"
|
||||
],
|
||||
"valuePermissionHighestLevels": {
|
||||
"assignmentPermission": "all",
|
||||
@@ -190,7 +191,8 @@
|
||||
"dataPrivacyPermission": "yes",
|
||||
"auditPermission": "yes",
|
||||
"mentionPermission": "yes",
|
||||
"userCalendarPermission": "all"
|
||||
"userCalendarPermission": "all",
|
||||
"lockPermission": "yes"
|
||||
},
|
||||
"permissionsStrictDefaults": {
|
||||
"assignmentPermission": "no",
|
||||
@@ -204,6 +206,7 @@
|
||||
"dataPrivacyPermission": "no",
|
||||
"auditPermission": "no",
|
||||
"mentionPermission": "no",
|
||||
"userCalendarPermission": "no"
|
||||
"userCalendarPermission": "no",
|
||||
"lockPermission": "no"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,11 @@
|
||||
},
|
||||
"merge": {
|
||||
"implementationClassName": "Espo\\Core\\Action\\Actions\\Merge"
|
||||
},
|
||||
"lock": {
|
||||
"implementationClassName": "Espo\\Core\\Action\\Actions\\Lock"
|
||||
},
|
||||
"unlock": {
|
||||
"implementationClassName": "Espo\\Core\\Action\\Actions\\Unlock"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"Espo\\Tools\\EntityManager\\Hook\\Hooks\\AssignedUsersUpdateHook",
|
||||
"Espo\\Tools\\EntityManager\\Hook\\Hooks\\CollaboratorsUpdateHook",
|
||||
"Espo\\Tools\\EntityManager\\Hook\\Hooks\\StreamUpdateHook",
|
||||
"Espo\\Tools\\EntityManager\\Hook\\Hooks\\CategoriesUpdateHook"
|
||||
"Espo\\Tools\\EntityManager\\Hook\\Hooks\\CategoriesUpdateHook",
|
||||
"Espo\\Tools\\EntityManager\\Hook\\Hooks\\LockableUpdateHook"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -69,6 +69,13 @@
|
||||
"tooltip": true,
|
||||
"view": "views/admin/entity-manager/fields/acl-account-link"
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@Person": {
|
||||
@@ -118,6 +125,13 @@
|
||||
"tooltip": true,
|
||||
"view": "views/admin/entity-manager/fields/acl-account-link"
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@Base": {
|
||||
@@ -174,6 +188,13 @@
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@BasePlus": {
|
||||
@@ -230,6 +251,13 @@
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
},
|
||||
"lockable": {
|
||||
"location": "scopes",
|
||||
"fieldDefs": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,11 @@
|
||||
},
|
||||
"delete": {
|
||||
"implementationClassName": "Espo\\Core\\MassAction\\Actions\\MassDelete"
|
||||
},
|
||||
"lock": {
|
||||
"implementationClassName": "Espo\\Core\\MassAction\\Actions\\MassLock"
|
||||
},
|
||||
"unlock": {
|
||||
"implementationClassName": "Espo\\Core\\MassAction\\Actions\\MassUnlock"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"selectApplierClassNameList": [
|
||||
"Espo\\Core\\Select\\Applier\\AdditionalAppliers\\IsStarred"
|
||||
],
|
||||
"beforeCreateHookClassNameList": [
|
||||
"Espo\\Classes\\Record\\Common\\Lock\\BeforeSaveValidation"
|
||||
],
|
||||
"beforeUpdateHookClassNameList": [
|
||||
"Espo\\Classes\\Record\\Common\\Lock\\BeforeSaveValidation"
|
||||
],
|
||||
"beforeDeleteHookClassNameList": [
|
||||
"Espo\\Classes\\Record\\Common\\Lock\\BeforeDeleteValidation"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,6 +15,56 @@
|
||||
"checkVisibilityFunction": "isAvailable",
|
||||
"handler": "handlers/record/view-user-access",
|
||||
"groupIndex": 4
|
||||
},
|
||||
{
|
||||
"name": "lock",
|
||||
"acl": "edit",
|
||||
"label": "Lock",
|
||||
"handler": "handlers/record/lock-action",
|
||||
"checkVisibilityFunction": "canBeLocked",
|
||||
"actionFunction": "actionLock",
|
||||
"groupIndex": 9
|
||||
},
|
||||
{
|
||||
"name": "unlock",
|
||||
"acl": "edit",
|
||||
"label": "Unlock",
|
||||
"handler": "handlers/record/lock-action",
|
||||
"checkVisibilityFunction": "canBeUnlocked",
|
||||
"actionFunction": "actionUnlock",
|
||||
"groupIndex": 9
|
||||
}
|
||||
]
|
||||
],
|
||||
"massActionList": [
|
||||
"lock",
|
||||
"unlock"
|
||||
],
|
||||
"checkAllResultMassActionList": [
|
||||
"lock",
|
||||
"unlock"
|
||||
],
|
||||
"massActionDefs": {
|
||||
"lock": {
|
||||
"acl": "edit",
|
||||
"handler": "handlers/record/lock-mass-action",
|
||||
"actionFunction": "actionLock",
|
||||
"initFunction": "initLock",
|
||||
"groupIndex": 9
|
||||
},
|
||||
"unlock": {
|
||||
"acl": "edit",
|
||||
"handler": "handlers/record/lock-mass-action",
|
||||
"actionFunction": "actionUnlock",
|
||||
"initFunction": "initUnlock",
|
||||
"groupIndex": 9
|
||||
}
|
||||
},
|
||||
"viewSetupHandlers": {
|
||||
"record/detail": [
|
||||
"handlers/record/lock-view-setup"
|
||||
],
|
||||
"record/edit": [
|
||||
"handlers/record/lock-view-setup"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,15 @@
|
||||
"view": "views/role/fields/permission",
|
||||
"audited": true
|
||||
},
|
||||
"lockPermission": {
|
||||
"type": "enum",
|
||||
"options": ["not-set", "yes", "no"],
|
||||
"default": "not-set",
|
||||
"tooltip": true,
|
||||
"translation": "Role.options.levelList",
|
||||
"view": "views/role/fields/permission",
|
||||
"audited": true
|
||||
},
|
||||
"data": {
|
||||
"type": "jsonObject",
|
||||
"audited": true
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EntityManager\Hook\Hooks;
|
||||
|
||||
use Espo\Core\DataManager;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Modules\Crm\Entities\Account;
|
||||
use Espo\ORM\Defs\Params\FieldParam;
|
||||
use Espo\Tools\EntityManager\Hook\UpdateHook;
|
||||
use Espo\Tools\EntityManager\Params;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class LockableUpdateHook implements UpdateHook
|
||||
{
|
||||
private const string PARAM = 'lockable';
|
||||
private const string FIELD = Field::IS_LOCKED;
|
||||
|
||||
/** @var string[] */
|
||||
private array $enabledByDefaultEntityTypeList = [
|
||||
Account::ENTITY_TYPE,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private Log $log,
|
||||
private DataManager $dataManager,
|
||||
) {}
|
||||
|
||||
public function process(Params $params, Params $previousParams): void
|
||||
{
|
||||
if ($params->get(self::PARAM) && !$previousParams->get(self::PARAM)) {
|
||||
$this->add($params->getName());
|
||||
} else if (!$params->get(self::PARAM) && $previousParams->get(self::PARAM)) {
|
||||
$this->remove($params->getName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
private function add(string $entityType): void
|
||||
{
|
||||
if ($this->metadata->get("entityDefs.$entityType.fields." . self::FIELD . ".isCustom")) {
|
||||
$this->log->warning("Cannot enable lockable for $entityType as the field already exists.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->isEnabledByDefault($entityType)) {
|
||||
$this->addEnabledByDefault($entityType);
|
||||
} else {
|
||||
$this->addInternal($entityType);
|
||||
}
|
||||
|
||||
|
||||
$this->metadata->save();
|
||||
$this->dataManager->rebuild([$entityType]);
|
||||
}
|
||||
|
||||
private function addEnabledByDefault(string $entityType): void
|
||||
{
|
||||
$this->metadata->delete('entityDefs', $entityType, [
|
||||
'fields.' . self::FIELD,
|
||||
]);
|
||||
}
|
||||
|
||||
private function addInternal(string $entityType): void
|
||||
{
|
||||
$this->metadata->set('entityDefs', $entityType, [
|
||||
'fields' => [
|
||||
self::FIELD => [
|
||||
FieldParam::TYPE => FieldType::BOOL,
|
||||
FieldParam::READ_ONLY => true,
|
||||
'audited' => true,
|
||||
'fieldManagerParamList' => [
|
||||
'audited',
|
||||
'tooltipText',
|
||||
],
|
||||
'layoutAvailabilityList' => [
|
||||
'filters',
|
||||
'list',
|
||||
],
|
||||
],
|
||||
]
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
private function remove(string $entityType): void
|
||||
{
|
||||
$this->metadata->delete('entityDefs', $entityType, [
|
||||
'fields.' . self::FIELD,
|
||||
]);
|
||||
|
||||
$this->metadata->save();
|
||||
|
||||
// Must be after metadata is saved.
|
||||
if ($this->isEnabledByDefault($entityType)) {
|
||||
$this->metadata->set('entityDefs', $entityType, [
|
||||
'fields' => [
|
||||
self::FIELD => [
|
||||
FieldParam::DISABLED => true,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->metadata->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function isEnabledByDefault(string $entityType): bool
|
||||
{
|
||||
return in_array($entityType, $this->enabledByDefaultEntityTypeList);
|
||||
}
|
||||
}
|
||||
@@ -156,6 +156,7 @@ class NameUtil
|
||||
Field::CREATED_AT,
|
||||
Field::MODIFIED_BY,
|
||||
Field::MODIFIED_AT,
|
||||
Field::IS_LOCKED,
|
||||
|
||||
'emailAddressList',
|
||||
'userEmailAddressList',
|
||||
|
||||
@@ -705,6 +705,12 @@ class FieldManager
|
||||
],
|
||||
];
|
||||
|
||||
if ($this->metadata->get("scopes.$scope.lockable") === true) {
|
||||
$additionalParamList['notLockable'] = [
|
||||
FieldParam::TYPE => FieldType::BOOL,
|
||||
];
|
||||
}
|
||||
|
||||
$type = $fieldDefs[FieldParam::TYPE] ?? null;
|
||||
|
||||
if (!$type) {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Lock;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
class LockMetadataProvider
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
) {}
|
||||
|
||||
public function isEnabled(string $entityType): bool
|
||||
{
|
||||
if (!$this->metadata->get("scopes.$entityType.lockable")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->metadata->get("entityDefs.$entityType.fields.isLocked")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Lock;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\ErrorSilent;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\MassAction\Result;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\ORM\Collection;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class LockService
|
||||
{
|
||||
public function __construct(
|
||||
private Acl $acl,
|
||||
private EntityManager $entityManager,
|
||||
private LockMetadataProvider $lockMetadataProvider,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param Collection<Entity> $collection
|
||||
*/
|
||||
public function massLock(Collection $collection): Result
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
foreach ($collection as $entity) {
|
||||
try {
|
||||
$this->lock($entity);
|
||||
} catch (Error|Forbidden|BadRequest) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$count ++;
|
||||
}
|
||||
|
||||
return new Result(count: $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<Entity> $collection
|
||||
*/
|
||||
public function massUnlock(Collection $collection): Result
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
foreach ($collection as $entity) {
|
||||
try {
|
||||
$this->unlock($entity);
|
||||
} catch (Error|Forbidden|BadRequest) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$count ++;
|
||||
}
|
||||
|
||||
return new Result(count: $count);
|
||||
}
|
||||
|
||||
public function isEnabled(string $entityType): bool
|
||||
{
|
||||
return $this->lockMetadataProvider->isEnabled($entityType);
|
||||
}
|
||||
|
||||
public function isAllowed(string $entityType): bool
|
||||
{
|
||||
if (!$this->acl->checkScope($entityType, Acl\Table::ACTION_EDIT)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->acl->getPermissionLevel(Acl\Permission::LOCK) !== Acl\Table::LEVEL_YES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function lock(Entity $entity): void
|
||||
{
|
||||
$this->check($entity);
|
||||
|
||||
if ($entity->get(Field::IS_LOCKED)) {
|
||||
throw new BadRequest("Already locked.");
|
||||
}
|
||||
|
||||
$this->lockEntity($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function unlock(Entity $entity): void
|
||||
{
|
||||
$this->check($entity);
|
||||
|
||||
if (!$entity->get(Field::IS_LOCKED)) {
|
||||
throw new BadRequest("Already unlocked.");
|
||||
}
|
||||
|
||||
$this->unlockEntity($entity);
|
||||
}
|
||||
|
||||
private function lockEntity(Entity $entity): void
|
||||
{
|
||||
if ($entity->get(Field::IS_LOCKED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entity->set(Field::IS_LOCKED, true);
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
|
||||
}
|
||||
|
||||
private function unlockEntity(Entity $entity): void
|
||||
{
|
||||
if (!$entity->get(Field::IS_LOCKED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entity->set(Field::IS_LOCKED, false);
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function check(Entity $entity): void
|
||||
{
|
||||
if (!$this->isEnabled($entity->getEntityType())) {
|
||||
throw new ErrorSilent("Lock is not enabled.");
|
||||
}
|
||||
|
||||
if ($this->acl->getPermissionLevel(Acl\Permission::LOCK) !== Acl\Table::LEVEL_YES) {
|
||||
throw new ForbiddenSilent("Not lock permission.");
|
||||
}
|
||||
|
||||
if (!$this->acl->checkEntityEdit($entity)) {
|
||||
throw new ForbiddenSilent("No edit access.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Lock;
|
||||
|
||||
use Espo\Core\Acl\AssignmentChecker\Helper;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\ConflictSilent;
|
||||
use Espo\Core\Exceptions\Error\Body;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
use Espo\Core\Utils\Language;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Defs\Params\FieldParam;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class LockValidationHelper
|
||||
{
|
||||
private const string PARAM_NOT_LOCKABLE = 'notLockable';
|
||||
|
||||
/** @var string[] */
|
||||
private array $ignoreFieldList = [
|
||||
Field::MODIFIED_AT,
|
||||
Field::MODIFIED_BY,
|
||||
Field::IS_LOCKED,
|
||||
Field::STREAM_UPDATED_AT,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private LockMetadataProvider $lockMetadataProvider,
|
||||
private FieldUtil $fieldUtil,
|
||||
private Defs $defs,
|
||||
private Log $log,
|
||||
private Language $language,
|
||||
private Helper $assignmentHelper,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function validateBeforeSave(Entity $entity): void
|
||||
{
|
||||
if ($entity->isNew()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->lockMetadataProvider->isEnabled($entity->getEntityType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$entity->getFetched(Field::IS_LOCKED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$changedField = $this->getChangedLockedField($entity);
|
||||
|
||||
if ($changedField === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->log->info("Cannot modify a locked record, '{field}' cannot be changed.", ['field' => $changedField]);
|
||||
|
||||
$fieldLabel = $this->language->translateLabel($changedField, 'fields', $entity->getEntityType());
|
||||
|
||||
throw ConflictSilent::createWithBody(
|
||||
'cannotModifyLockedRecord',
|
||||
Body::create()->withMessageTranslation('cannotModifyLockedRecord', data: ['field' => $fieldLabel])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function validateBeforeRemove(Entity $entity): void
|
||||
{
|
||||
if (!$this->lockMetadataProvider->isEnabled($entity->getEntityType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$entity->get(Field::IS_LOCKED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw ConflictSilent::createWithBody(
|
||||
'cannotRemoveLockedRecord',
|
||||
Body::create()->withMessageTranslation('cannotRemoveLockedRecord')
|
||||
);
|
||||
}
|
||||
|
||||
private function getChangedLockedField(Entity $entity): ?string
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$entityDefs = $this->defs->getEntity($entityType);
|
||||
|
||||
$ignoreFieldList = $this->getIgnoreFieldList($entityType);
|
||||
|
||||
foreach ($entityDefs->getFieldList() as $fieldDefs) {
|
||||
$field = $fieldDefs->getName();
|
||||
|
||||
if (
|
||||
$fieldDefs->getParam(FieldParam::READ_ONLY) ||
|
||||
$fieldDefs->getParam(FieldParam::READ_ONLY_AFTER_CREATE)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($field, $ignoreFieldList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($fieldDefs->getParam(self::PARAM_NOT_LOCKABLE)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->fieldUtil->getActualAttributeList($entityType, $field) as $attribute) {
|
||||
if ($entity->isAttributeChanged($attribute)) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getIgnoreFieldList(string $entityType): array
|
||||
{
|
||||
$ignoreFieldList = $this->ignoreFieldList;
|
||||
|
||||
$entityDefs = $this->defs->getEntity($entityType);
|
||||
|
||||
if ($this->assignmentHelper->hasCollaboratorsField($entityType)) {
|
||||
if ($this->assignmentHelper->hasAssignedUsersField($entityType)) {
|
||||
if (
|
||||
$entityDefs->tryGetField(Field::ASSIGNED_USERS)
|
||||
?->getParam(self::PARAM_NOT_LOCKABLE)
|
||||
) {
|
||||
$ignoreFieldList[] = Field::COLLABORATORS;
|
||||
}
|
||||
} else if ($this->assignmentHelper->hasAssignedUserField($entityType)) {
|
||||
if (
|
||||
$entityDefs->tryGetField(Field::ASSIGNED_USER)
|
||||
?->getParam(self::PARAM_NOT_LOCKABLE)
|
||||
) {
|
||||
$ignoreFieldList[] = Field::COLLABORATORS;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $ignoreFieldList;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user