Initial commit
This commit is contained in:
746
application/Espo/Core/Record/Access/LinkCheck.php
Normal file
746
application/Espo/Core/Record/Access/LinkCheck.php
Normal file
@@ -0,0 +1,746 @@
|
||||
<?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\Record\Access;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\LinkChecker;
|
||||
use Espo\Core\Acl\LinkChecker\LinkCheckerFactory;
|
||||
use Espo\Core\Acl\Table as AclTable;
|
||||
use Espo\Core\Exceptions\Error\Body as ErrorBody;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\ORM\Defs\AttributeParam;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Crm\Entities\Account;
|
||||
use Espo\Modules\Crm\Entities\Contact;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Defs\EntityDefs;
|
||||
use Espo\ORM\Defs\FieldDefs;
|
||||
use Espo\ORM\Defs\RelationDefs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Type\AttributeType;
|
||||
use Espo\ORM\Type\RelationType;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Check access for record linking. When linking directly through relationships or via link fields.
|
||||
* Also loads foreign name attributes.
|
||||
*/
|
||||
class LinkCheck
|
||||
{
|
||||
/** @var array<string, LinkChecker<Entity, Entity>> */
|
||||
private $linkCheckerCache = [];
|
||||
|
||||
/** @var string[] */
|
||||
private array $oneFieldTypeList = [
|
||||
FieldType::LINK,
|
||||
FieldType::LINK_PARENT,
|
||||
FieldType::LINK_ONE,
|
||||
FieldType::FILE,
|
||||
FieldType::IMAGE,
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private array $manyFieldTypeList = [
|
||||
FieldType::LINK_MULTIPLE,
|
||||
FieldType::ATTACHMENT_MULTIPLE,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private Defs $ormDefs,
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl,
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private User $user,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Checks relation fields set in an entity (link-multiple, link and others).
|
||||
*
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function processFields(Entity $entity): void
|
||||
{
|
||||
$this->processLinkMultipleFields($entity);
|
||||
$this->processLinkFields($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function processLinkMultipleFields(Entity $entity): void
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$entityDefs = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType);
|
||||
|
||||
$typeList = [
|
||||
Entity::HAS_MANY,
|
||||
Entity::MANY_MANY,
|
||||
Entity::HAS_CHILDREN,
|
||||
];
|
||||
|
||||
foreach ($entityDefs->getRelationList() as $relationDefs) {
|
||||
$name = $relationDefs->getName();
|
||||
|
||||
if (!in_array($relationDefs->getType(), $typeList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attribute = $name . 'Ids';
|
||||
$namesAttribute = $name . 'Names';
|
||||
|
||||
if (
|
||||
!$entityDefs->hasAttribute($attribute) ||
|
||||
!$entity->isAttributeChanged($attribute)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var string[] $ids */
|
||||
$ids = $entity->get($attribute) ?? [];
|
||||
/** @var string[] $oldIds */
|
||||
$oldIds = $entity->getFetched($attribute) ?? [];
|
||||
|
||||
$setIds = $ids;
|
||||
$ids = array_values(array_diff($ids, $oldIds));
|
||||
$removedIds = array_values(array_diff($oldIds, $ids));
|
||||
|
||||
if ($ids === [] && $removedIds === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->processCheckLinkWithoutField($entityDefs, $name, false, $setIds);
|
||||
|
||||
$names = $this->prepareNames($entity, $namesAttribute, $setIds);
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$foreignEntity = $this->processLinkedRecordsCheckItem($entity, $relationDefs, $id);
|
||||
|
||||
if ($foreignEntity) {
|
||||
$names->$id = $foreignEntity->get(Field::NAME);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$entityDefs->tryGetAttribute($namesAttribute)?->getParam(AttributeParam::IS_LINK_MULTIPLE_NAME_MAP)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entity->set($namesAttribute, $names);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string[] $ids
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function processCheckLinkWithoutField(
|
||||
EntityDefs $entityDefs,
|
||||
string $name,
|
||||
bool $isOne,
|
||||
?array $ids = null
|
||||
): void {
|
||||
|
||||
$fieldTypes = $isOne ? $this->oneFieldTypeList : $this->manyFieldTypeList;
|
||||
|
||||
$hasField =
|
||||
$entityDefs->hasField($name) &&
|
||||
in_array($entityDefs->getField($name)->getType(), $fieldTypes);
|
||||
|
||||
if ($hasField) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($isOne) {
|
||||
throw new ForbiddenSilent("Cannot set ID attribute for link '$name' as there's no link field.");
|
||||
}
|
||||
|
||||
if ($ids !== null && count($ids) > 1) {
|
||||
throw new ForbiddenSilent("Cannot set multiple IDs for link '$name' as there's no link-multiple field.");
|
||||
}
|
||||
|
||||
$forbiddenLinkList = $this->acl->getScopeForbiddenLinkList($entityDefs->getName(), AclTable::ACTION_EDIT);
|
||||
|
||||
if (!in_array($name, $forbiddenLinkList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No access to link $name.",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('cannotRelateForbiddenLink', null, ['link' => $name])
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function processLinkedRecordsCheckItem(
|
||||
Entity $entity,
|
||||
RelationDefs $defs,
|
||||
string $id,
|
||||
bool $isOne = false
|
||||
): ?Entity {
|
||||
|
||||
$entityType = $entity->getEntityType();
|
||||
$link = $defs->getName();
|
||||
|
||||
if ($this->getParam($entityType, $link, 'linkCheckDisabled')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$foreignEntityType = null;
|
||||
|
||||
if ($defs->getType() === RelationType::BELONGS_TO_PARENT) {
|
||||
$foreignEntityType = $entity->get($link . 'Type');
|
||||
}
|
||||
|
||||
if (!$foreignEntityType && !$defs->hasForeignEntityType()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$foreignEntityType ??= $defs->getForeignEntityType();
|
||||
|
||||
$foreignEntity = $this->entityManager->getEntityById($foreignEntityType, $id);
|
||||
|
||||
if (!$foreignEntity) {
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"Can't relate with non-existing record. entity type: $entityType, link: $link.",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation(
|
||||
'cannotRelateNonExisting', null, ['foreignEntityType' => $foreignEntityType])
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
|
||||
$toSkip = $this->linkForeignAccessCheck($isOne, $entityType, $link, $foreignEntity);
|
||||
|
||||
if ($toSkip) {
|
||||
return $foreignEntity;
|
||||
}
|
||||
|
||||
$this->linkEntityAccessCheck($entity, $foreignEntity, $link);
|
||||
|
||||
return $foreignEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function linkForeignAccessCheck(
|
||||
bool $isOne,
|
||||
string $entityType,
|
||||
string $link,
|
||||
Entity $foreignEntity
|
||||
): bool {
|
||||
|
||||
if ($isOne) {
|
||||
return $this->linkForeignAccessCheckOne($entityType, $link, $foreignEntity);
|
||||
}
|
||||
|
||||
return $this->linkForeignAccessCheckMany($entityType, $link, $foreignEntity, true);
|
||||
}
|
||||
|
||||
private function getParam(string $entityType, string $link, string $param): mixed
|
||||
{
|
||||
return $this->metadata->get(['recordDefs', $entityType, 'relationships', $link, $param]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access to a specific link.
|
||||
*
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function processLink(Entity $entity, string $link): void
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
/** @var AclTable::ACTION_*|null $action */
|
||||
$action = $this->getParam($entityType, $link, 'linkRequiredAccess');
|
||||
|
||||
if (!$action) {
|
||||
$action = AclTable::ACTION_EDIT;
|
||||
}
|
||||
|
||||
if (!$this->acl->check($entity, $action)) {
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No record access for link operation ($entityType:$link).",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('noAccessToRecord', null, ['action' => $action])
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check unlink access to a specific link.
|
||||
*
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function processUnlink(Entity $entity, string $link): void
|
||||
{
|
||||
$this->processLink($entity, $link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check link access for a specific foreign entity.
|
||||
*
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function processLinkForeign(Entity $entity, string $link, Entity $foreignEntity): void
|
||||
{
|
||||
$this->processLinkForeignInternal($entity, $link, $foreignEntity);
|
||||
$this->processLinkAlreadyLinkedCheck($entity, $link, $foreignEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check link access for a specific foreign entity.
|
||||
*
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function processLinkForeignInternal(Entity $entity, string $link, Entity $foreignEntity): void
|
||||
{
|
||||
$toSkip = $this->linkForeignAccessCheckMany($entity->getEntityType(), $link, $foreignEntity);
|
||||
|
||||
if ($toSkip) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->linkEntityAccessCheck($entity, $foreignEntity, $link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check unlink access for a specific foreign entity.
|
||||
*
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function processUnlinkForeign(Entity $entity, string $link, Entity $foreignEntity): void
|
||||
{
|
||||
$this->processLinkForeignInternal($entity, $link, $foreignEntity);
|
||||
$this->processUnlinkForeignRequired($entity, $link, $foreignEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access to foreign record for has-many and many-many links.
|
||||
*
|
||||
* @return bool True indicates that the link checker should be bypassed.
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function linkForeignAccessCheckMany(
|
||||
string $entityType,
|
||||
string $link,
|
||||
Entity $foreignEntity,
|
||||
bool $fromUpdate = false
|
||||
): bool {
|
||||
|
||||
/** @var AclTable::ACTION_* $action */
|
||||
$action = $this->getParam($entityType, $link, 'linkRequiredForeignAccess') ?? AclTable::ACTION_EDIT;
|
||||
|
||||
if ($this->getParam($entityType, $link, 'linkForeignAccessCheckDisabled')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$fieldDefs = $fromUpdate ?
|
||||
$this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType)
|
||||
->tryGetField($link) :
|
||||
null;
|
||||
|
||||
if (
|
||||
$fromUpdate &&
|
||||
$fieldDefs &&
|
||||
in_array($fieldDefs->getType(), $this->manyFieldTypeList)
|
||||
) {
|
||||
$action = AclTable::ACTION_READ;
|
||||
|
||||
if ($this->checkInDefaults($fieldDefs, $link, $foreignEntity)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
$action === AclTable::ACTION_READ &&
|
||||
$this->checkIsAllowedForPortal($foreignEntity)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->acl->check($foreignEntity, $action)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->getLinkChecker($entityType, $link)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = ErrorBody::create();
|
||||
|
||||
$body = $fromUpdate ?
|
||||
$body->withMessageTranslation('cannotRelateForbidden', null, [
|
||||
'foreignEntityType' => $foreignEntity->getEntityType(),
|
||||
'action' => $action,
|
||||
]) :
|
||||
$body->withMessageTranslation('noAccessToForeignRecord', null, ['action' => $action]);
|
||||
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No foreign record access for link operation ($entityType:$link).",
|
||||
$body->encode()
|
||||
);
|
||||
}
|
||||
|
||||
public function checkIsAllowedForPortal(Entity $foreignEntity): bool
|
||||
{
|
||||
if (!$this->user->isPortal()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
$foreignEntity->getEntityType() === Account::ENTITY_TYPE &&
|
||||
$this->user->getAccounts()->hasId($foreignEntity->getId())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
$foreignEntity->getEntityType() === Contact::ENTITY_TYPE &&
|
||||
$this->user->getContactId() === $foreignEntity->getId()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function linkEntityAccessCheck(Entity $entity, Entity $foreignEntity, string $link): void
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$checker = $this->getLinkChecker($entityType, $link);
|
||||
|
||||
if (!$checker) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($checker->check($this->user, $entity, $foreignEntity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No access for link operation ($entityType:$link).",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('noLinkAccess', null, [
|
||||
'foreignEntityType' => $foreignEntity->getEntityType(),
|
||||
'link' => $link,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?LinkChecker<Entity, Entity>
|
||||
*/
|
||||
private function getLinkChecker(string $entityType, string $link): ?LinkChecker
|
||||
{
|
||||
$key = $entityType . '_' . $link;
|
||||
|
||||
if (array_key_exists($key, $this->linkCheckerCache)) {
|
||||
return $this->linkCheckerCache[$key];
|
||||
}
|
||||
|
||||
$factory = $this->injectableFactory->create(LinkCheckerFactory::class);
|
||||
|
||||
if (!$factory->isCreatable($entityType, $link)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$checker = $factory->create($entityType, $link);
|
||||
|
||||
$this->linkCheckerCache[$link] = $checker;
|
||||
|
||||
return $checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function processUnlinkForeignRequired(Entity $entity, string $link, Entity $foreignEntity): void
|
||||
{
|
||||
$relationDefs = $this->ormDefs
|
||||
->getEntity($entity->getEntityType())
|
||||
->tryGetRelation($link);
|
||||
|
||||
if (!$relationDefs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!$relationDefs->hasForeignEntityType() ||
|
||||
!$relationDefs->hasForeignRelationName()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$foreignLink = $relationDefs->getForeignRelationName();
|
||||
|
||||
$foreignRelationDefs = $this->ormDefs
|
||||
->getEntity($foreignEntity->getEntityType())
|
||||
->tryGetRelation($foreignLink);
|
||||
|
||||
if (!$foreignRelationDefs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!in_array($foreignRelationDefs->getType(), [
|
||||
RelationType::BELONGS_TO,
|
||||
RelationType::HAS_ONE,
|
||||
RelationType::BELONGS_TO_PARENT,
|
||||
])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$foreignFieldDefs = $this->ormDefs
|
||||
->getEntity($foreignEntity->getEntityType())
|
||||
->tryGetField($foreignLink);
|
||||
|
||||
if (!$foreignFieldDefs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$foreignFieldDefs->getParam('required')) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"Can't unlink required field ({$foreignEntity->getEntityType()}:$foreignLink}).",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('cannotUnrelateRequiredLink')
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function processLinkFields(Entity $entity): void
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$entityDefs = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType);
|
||||
|
||||
$typeList = [
|
||||
Entity::BELONGS_TO,
|
||||
Entity::BELONGS_TO_PARENT,
|
||||
Entity::HAS_ONE,
|
||||
];
|
||||
|
||||
foreach ($entityDefs->getRelationList() as $relationDefs) {
|
||||
$name = $relationDefs->getName();
|
||||
$attribute = $name . 'Id';
|
||||
$nameAttribute = $name . 'Name';
|
||||
|
||||
if (
|
||||
!in_array($relationDefs->getType(), $typeList) ||
|
||||
!$entityDefs->hasAttribute($attribute) ||
|
||||
!$entity->isAttributeChanged($attribute) ||
|
||||
$entity->get($attribute) === null
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->processCheckLinkWithoutField($entityDefs, $name, true);
|
||||
|
||||
$id = $entity->get($attribute);
|
||||
|
||||
$foreignEntity = $this->processLinkedRecordsCheckItem($entity, $relationDefs, $id, true);
|
||||
|
||||
if (!$foreignEntity) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$nameAttributeDefs = $entityDefs->tryGetAttribute($nameAttribute);
|
||||
|
||||
if (!$nameAttributeDefs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
$nameAttributeDefs->getType() === AttributeType::FOREIGN ||
|
||||
$nameAttributeDefs->isNotStorable()
|
||||
) {
|
||||
$foreignName = $relationDefs->getParam('foreignName') ?? 'name';
|
||||
|
||||
$entity->set($nameAttribute, $foreignEntity->get($foreignName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access to foreign record for belongs-to, has-one and belongs-to-parent links.
|
||||
*
|
||||
* @return bool True indicates that the link checker should be bypassed.
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function linkForeignAccessCheckOne(string $entityType, string $link, Entity $foreignEntity): bool
|
||||
{
|
||||
if ($this->getParam($entityType, $link, 'linkForeignAccessCheckDisabled')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$fieldDefs = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType)
|
||||
->tryGetField($link);
|
||||
|
||||
if (
|
||||
$fieldDefs &&
|
||||
in_array($fieldDefs->getType(), $this->oneFieldTypeList)
|
||||
) {
|
||||
if ($this->checkIsDefault($fieldDefs, $link, $foreignEntity)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->checkIsAllowedForPortal($foreignEntity)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->acl->check($foreignEntity, AclTable::ACTION_READ)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->getLinkChecker($entityType, $link)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"No foreign record access for link operation ($entityType:$link).",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('cannotRelateForbidden', null, [
|
||||
'foreignEntityType' => $foreignEntity->getEntityType(),
|
||||
'action' => AclTable::ACTION_READ,
|
||||
])
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
|
||||
private function checkInDefaults(FieldDefs $fieldDefs, string $link, Entity $foreignEntity): bool
|
||||
{
|
||||
/** @var string[] $defaults */
|
||||
$defaults = $this->getDefault($fieldDefs, $link . 'Ids') ?? [];
|
||||
|
||||
return in_array($foreignEntity->getId(), $defaults);
|
||||
}
|
||||
|
||||
private function checkIsDefault(FieldDefs $fieldDefs, string $link, Entity $foreignEntity): bool
|
||||
{
|
||||
return $foreignEntity->getId() === $this->getDefault($fieldDefs, $link . 'Id');
|
||||
}
|
||||
private function getDefault(FieldDefs $fieldDefs, string $attribute): mixed
|
||||
{
|
||||
$defaultAttributes = (object) ($fieldDefs->getParam('defaultAttributes') ?? []);
|
||||
|
||||
return $defaultAttributes->$attribute ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $setIds
|
||||
*/
|
||||
private function prepareNames(Entity $entity, string $namesAttribute, array $setIds): stdClass
|
||||
{
|
||||
$oldNames = $entity->getFetched($namesAttribute);
|
||||
|
||||
if (!$oldNames instanceof stdClass) {
|
||||
$oldNames = (object) [];
|
||||
}
|
||||
|
||||
$names = (object) [];
|
||||
|
||||
foreach ($setIds as $id) {
|
||||
if (isset($oldNames->$id)) {
|
||||
$names->$id = $oldNames->$id;
|
||||
}
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function processLinkAlreadyLinkedCheck(Entity $entity, string $link, Entity $foreignEntity): void
|
||||
{
|
||||
if (!$this->getParam($entity->getEntityType(), $link, 'linkOnlyNotLinked')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$foreign = $this->ormDefs
|
||||
->getEntity($entityType)
|
||||
->tryGetRelation($link)
|
||||
?->tryGetForeignRelationName();
|
||||
|
||||
if (!$foreign) {
|
||||
return;
|
||||
}
|
||||
|
||||
$one = $this->entityManager
|
||||
->getRDBRepository($foreignEntity->getEntityType())
|
||||
->getRelation($foreignEntity, $foreign)
|
||||
->findOne();
|
||||
|
||||
if (!$one) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw ForbiddenSilent::createWithBody(
|
||||
"Cannot link as the record is already linked ($entityType:$link).",
|
||||
ErrorBody::create()
|
||||
->withMessageTranslation('cannotLinkAlreadyLinked')
|
||||
->encode()
|
||||
);
|
||||
}
|
||||
}
|
||||
38
application/Espo/Core/Record/ActionHistory/Action.php
Normal file
38
application/Espo/Core/Record/ActionHistory/Action.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\ActionHistory;
|
||||
|
||||
class Action
|
||||
{
|
||||
public const CREATE = 'create';
|
||||
public const READ = 'read';
|
||||
public const UPDATE = 'update';
|
||||
public const DELETE = 'delete';
|
||||
}
|
||||
45
application/Espo/Core/Record/ActionHistory/ActionLogger.php
Normal file
45
application/Espo/Core/Record/ActionHistory/ActionLogger.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\ActionHistory;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* Logs actions users do with records.
|
||||
*/
|
||||
interface ActionLogger
|
||||
{
|
||||
/**
|
||||
* Log an action.
|
||||
*
|
||||
* @param Action::* $action
|
||||
*/
|
||||
public function log(string $action, Entity $entity): void;
|
||||
}
|
||||
@@ -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\Record\ActionHistory;
|
||||
|
||||
use Espo\Core\Field\LinkParent;
|
||||
use Espo\Entities\ActionHistoryRecord;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class DefaultActionLogger implements ActionLogger
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private User $user
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function log(string $action, Entity $entity): void
|
||||
{
|
||||
$historyRecord = $this->entityManager
|
||||
->getRepositoryByClass(ActionHistoryRecord::class)
|
||||
->getNew();
|
||||
|
||||
$historyRecord
|
||||
->setAction($action)
|
||||
->setUserId($this->user->getId())
|
||||
->setAuthTokenId($this->user->get('authTokenId'))
|
||||
->setAuthLogRecordId($this->user->get('authLogRecordId'))
|
||||
->setIpAddress($this->user->get('ipAddress'))
|
||||
->setTarget(LinkParent::createFromEntity($entity));
|
||||
|
||||
$this->entityManager->saveEntity($historyRecord);
|
||||
}
|
||||
}
|
||||
149
application/Espo/Core/Record/Collection.php
Normal file
149
application/Espo/Core/Record/Collection.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record;
|
||||
|
||||
use Espo\ORM\Collection as OrmCollection;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityCollection;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Contains an ORM collection and total number of records.
|
||||
*
|
||||
* @template-covariant TEntity of Entity
|
||||
*/
|
||||
class Collection
|
||||
{
|
||||
public const TOTAL_HAS_MORE = -1;
|
||||
public const TOTAL_HAS_NO_MORE = -2;
|
||||
|
||||
/**
|
||||
* @param OrmCollection<TEntity> $collection
|
||||
*/
|
||||
public function __construct(
|
||||
private OrmCollection $collection,
|
||||
private ?int $total = null
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get a total number of records in DB (that matches applied search parameters).
|
||||
*/
|
||||
public function getTotal(): ?int
|
||||
{
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ORM collection.
|
||||
*
|
||||
* @return OrmCollection<TEntity>
|
||||
*/
|
||||
public function getCollection(): OrmCollection
|
||||
{
|
||||
return $this->collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value map list.
|
||||
*
|
||||
* @return stdClass[]
|
||||
*/
|
||||
public function getValueMapList(): array
|
||||
{
|
||||
if (
|
||||
$this->collection instanceof EntityCollection &&
|
||||
!$this->collection->getEntityType()
|
||||
) {
|
||||
$list = [];
|
||||
|
||||
foreach ($this->collection as $e) {
|
||||
$item = $e->getValueMap();
|
||||
|
||||
$item->_scope = $e->getEntityType();
|
||||
|
||||
$list[] = $item;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
return $this->collection->getValueMapList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create.
|
||||
*
|
||||
* @template CEntity of Entity
|
||||
* @param OrmCollection<CEntity> $collection
|
||||
* @return self<CEntity>
|
||||
*/
|
||||
public static function create(OrmCollection $collection, ?int $total = null): self
|
||||
{
|
||||
return new self($collection, $total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create w/o count.
|
||||
*
|
||||
* @template CEntity of Entity
|
||||
* @param OrmCollection<CEntity> $collection
|
||||
* @return self<CEntity>
|
||||
*/
|
||||
public static function createNoCount(OrmCollection $collection, ?int $maxSize): self
|
||||
{
|
||||
if (
|
||||
$maxSize !== null &&
|
||||
$collection instanceof EntityCollection &&
|
||||
count($collection) > $maxSize
|
||||
) {
|
||||
$copyCollection = new EntityCollection([...$collection], $collection->getEntityType());
|
||||
|
||||
unset($copyCollection[count($copyCollection) - 1]);
|
||||
|
||||
return new self($copyCollection, self::TOTAL_HAS_MORE);
|
||||
}
|
||||
|
||||
return new self($collection, self::TOTAL_HAS_NO_MORE);
|
||||
}
|
||||
|
||||
/**
|
||||
* To API output. To be used in API actions.
|
||||
*
|
||||
* @since 9.1.0
|
||||
*/
|
||||
public function toApiOutput(): stdClass
|
||||
{
|
||||
return (object) [
|
||||
'total' => $this->getTotal(),
|
||||
'list' => $this->getValueMapList(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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\Record\ConcurrencyControl\Optimistic;
|
||||
|
||||
use stdClass;
|
||||
|
||||
readonly class Result
|
||||
{
|
||||
/**
|
||||
* @param string[] $fieldList Changed fields.
|
||||
* @param stdClass $values Previous values.
|
||||
* @param int $versionNumber A previous version number.
|
||||
*/
|
||||
public function __construct(
|
||||
public array $fieldList,
|
||||
public stdClass $values,
|
||||
public int $versionNumber,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\ConcurrencyControl;
|
||||
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Record\ConcurrencyControl\Optimistic\Result;
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
use Espo\ORM\BaseEntity;
|
||||
use Espo\ORM\Defs\Params\FieldParam;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class OptimisticProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private FieldUtil $fieldUtil,
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity, int $versionNumber): ?Result
|
||||
{
|
||||
$previousVersionNumber = $entity->getFetched(Field::VERSION_NUMBER);
|
||||
|
||||
if ($previousVersionNumber === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($versionNumber === $previousVersionNumber) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$changedFieldList = [];
|
||||
|
||||
$entityDefs = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entity->getEntityType());
|
||||
|
||||
foreach ($entityDefs->getFieldList() as $fieldDefs) {
|
||||
$field = $fieldDefs->getName();
|
||||
|
||||
if (
|
||||
$fieldDefs->getParam('optimisticConcurrencyControlIgnore') ||
|
||||
$fieldDefs->getParam(FieldParam::READ_ONLY)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->fieldUtil->getActualAttributeList($entityDefs->getName(), $field) as $attribute) {
|
||||
if (
|
||||
$entity instanceof BaseEntity &&
|
||||
!$entity->isAttributeWritten($attribute)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$entity->hasFetched($attribute)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($entity->isAttributeChanged($attribute)) {
|
||||
$changedFieldList[] = $field;
|
||||
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($changedFieldList === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$values = (object) [];
|
||||
|
||||
foreach ($changedFieldList as $field) {
|
||||
foreach ($this->fieldUtil->getAttributeList($entityDefs->getName(), $field) as $attribute) {
|
||||
$values->$attribute = $entity->getFetched($attribute);
|
||||
}
|
||||
}
|
||||
|
||||
return new Result(
|
||||
fieldList: $changedFieldList,
|
||||
values: $values,
|
||||
versionNumber: $previousVersionNumber,
|
||||
);
|
||||
}
|
||||
}
|
||||
74
application/Espo/Core/Record/CreateParams.php
Normal file
74
application/Espo/Core/Record/CreateParams.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record;
|
||||
|
||||
/**
|
||||
* Immutable.
|
||||
*/
|
||||
class CreateParams
|
||||
{
|
||||
private bool $skipDuplicateCheck = false;
|
||||
private ?string $duplicateSourceId = null;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function withSkipDuplicateCheck(bool $skipDuplicateCheck = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
|
||||
$obj->skipDuplicateCheck = $skipDuplicateCheck;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withDuplicateSourceId(?string $duplicateSourceId): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
|
||||
$obj->duplicateSourceId = $duplicateSourceId;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function skipDuplicateCheck(): bool
|
||||
{
|
||||
return $this->skipDuplicateCheck;
|
||||
}
|
||||
|
||||
public function getDuplicateSourceId(): ?string
|
||||
{
|
||||
return $this->duplicateSourceId;
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
}
|
||||
53
application/Espo/Core/Record/CreateParamsFetcher.php
Normal file
53
application/Espo/Core/Record/CreateParamsFetcher.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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\Record;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
class CreateParamsFetcher
|
||||
{
|
||||
public function __construct() {}
|
||||
|
||||
public function fetch(Request $request): CreateParams
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$skipDuplicateCheck = $request->hasHeader('X-Skip-Duplicate-Check') ?
|
||||
strtolower($request->getHeader('X-Skip-Duplicate-Check') ?? '') === 'true' :
|
||||
$data->_skipDuplicateCheck ?? // legacy
|
||||
false;
|
||||
|
||||
$duplicateSourceId = $request->getHeader('X-Duplicate-Source-Id');
|
||||
|
||||
return CreateParams::create()
|
||||
->withSkipDuplicateCheck($skipDuplicateCheck)
|
||||
->withDuplicateSourceId($duplicateSourceId);
|
||||
}
|
||||
}
|
||||
66
application/Espo/Core/Record/Crud.php
Normal file
66
application/Espo/Core/Record/Crud.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface Crud
|
||||
{
|
||||
/**
|
||||
* Create a record.
|
||||
*
|
||||
* @return TEntity
|
||||
*/
|
||||
public function create(stdClass $data, CreateParams $params): Entity;
|
||||
|
||||
/**
|
||||
* Read a record.
|
||||
*
|
||||
* @return TEntity
|
||||
*/
|
||||
public function read(string $id, ReadParams $params): Entity;
|
||||
|
||||
/**
|
||||
* Update a record.
|
||||
*
|
||||
* @return TEntity
|
||||
*/
|
||||
public function update(string $id, stdClass $data, UpdateParams $params): Entity;
|
||||
|
||||
/**
|
||||
* Delete a record.
|
||||
*/
|
||||
public function delete(string $id, DeleteParams $params): void;
|
||||
}
|
||||
278
application/Espo/Core/Record/Defaults/DefaultPopulator.php
Normal file
278
application/Espo/Core/Record/Defaults/DefaultPopulator.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Defaults;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Permission;
|
||||
use Espo\Core\Acl\Table as AclTable;
|
||||
use Espo\Core\Currency\ConfigDataProvider as CurrencyConfigDataProvider;
|
||||
use Espo\Core\Field\Link;
|
||||
use Espo\Core\Field\LinkMultiple;
|
||||
use Espo\Core\Field\LinkMultipleItem;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Crm\Entities\Account;
|
||||
use Espo\Modules\Crm\Entities\Contact;
|
||||
use Espo\ORM\Defs\Params\FieldParam;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @implements Populator<Entity>
|
||||
*/
|
||||
class DefaultPopulator implements Populator
|
||||
{
|
||||
public function __construct(
|
||||
private Acl $acl,
|
||||
private User $user,
|
||||
private FieldUtil $fieldUtil,
|
||||
private EntityManager $entityManager,
|
||||
private CurrencyConfigDataProvider $currencyConfig,
|
||||
private Metadata $metadata,
|
||||
) {}
|
||||
|
||||
public function populate(Entity $entity): void
|
||||
{
|
||||
$this->processAssignedUser($entity);
|
||||
$this->processDefaultTeam($entity);
|
||||
$this->processCurrency($entity);
|
||||
$this->processPortal($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* If no edit access to assignedUser field.
|
||||
*/
|
||||
private function isAssignedUserShouldBeSetWithSelf(string $entityType): bool
|
||||
{
|
||||
if ($this->user->isPortal()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$defs = $this->entityManager->getDefs()->getEntity($entityType);
|
||||
|
||||
if ($defs->tryGetField(Field::ASSIGNED_USER)?->getType() !== FieldType::LINK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->acl->getPermissionLevel(Permission::ASSIGNMENT) === AclTable::LEVEL_NO &&
|
||||
!$this->user->isApi()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->acl->checkField($entityType, Field::ASSIGNED_USER, AclTable::ACTION_EDIT)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function toAddDefaultTeam(Entity $entity): bool
|
||||
{
|
||||
if ($this->user->isPortal()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->user->getDefaultTeam()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$defs = $this->entityManager->getDefs()->getEntity($entityType);
|
||||
|
||||
if ($defs->tryGetField(Field::TEAMS)?->getType() !== FieldType::LINK_MULTIPLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($entity->hasLinkMultipleId(Field::TEAMS, $this->user->getDefaultTeam()->getId())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->acl->getPermissionLevel(Permission::ASSIGNMENT) === AclTable::LEVEL_NO) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->acl->checkField($entityType, Field::TEAMS, AclTable::ACTION_EDIT)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function processCurrency(Entity $entity): void
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
foreach ($this->fieldUtil->getEntityTypeFieldList($entityType) as $field) {
|
||||
$type = $this->fieldUtil->getEntityTypeFieldParam($entityType, $field, FieldParam::TYPE);
|
||||
|
||||
if ($type !== FieldType::CURRENCY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$currencyAttribute = $field . 'Currency';
|
||||
|
||||
if ($entity->get($field) !== null && !$entity->get($currencyAttribute)) {
|
||||
$entity->set($currencyAttribute, $this->currencyConfig->getDefaultCurrency());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function processDefaultTeam(Entity $entity): void
|
||||
{
|
||||
if (!$this->toAddDefaultTeam($entity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$defaultTeamId = $this->user->getDefaultTeam()?->getId();
|
||||
|
||||
if (!$defaultTeamId || !$entity instanceof CoreEntity) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$entity->addLinkMultipleId(Field::TEAMS, $defaultTeamId);
|
||||
|
||||
$teamsNames = $entity->get(Field::TEAMS . 'Names');
|
||||
|
||||
if (!$teamsNames || !is_object($teamsNames)) {
|
||||
$teamsNames = (object)[];
|
||||
}
|
||||
|
||||
$teamsNames->$defaultTeamId = $this->user->getDefaultTeam()?->getName();
|
||||
|
||||
$entity->set(Field::TEAMS . 'Names', $teamsNames);
|
||||
}
|
||||
|
||||
private function processAssignedUser(Entity $entity): void
|
||||
{
|
||||
if (!$this->isAssignedUserShouldBeSetWithSelf($entity->getEntityType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entity->set(Field::ASSIGNED_USER . 'Id', $this->user->getId());
|
||||
$entity->set(Field::ASSIGNED_USER . 'Name', $this->user->getName());
|
||||
}
|
||||
|
||||
private function processPortal(Entity $entity): void
|
||||
{
|
||||
if (!$this->user->isPortal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processPortalAccount($entity);
|
||||
$this->processPortalContact($entity);
|
||||
}
|
||||
|
||||
private function processPortalAccount(Entity $entity): void
|
||||
{
|
||||
/** @var ?string $link */
|
||||
$link = $this->metadata->get("aclDefs.{$entity->getEntityType()}.accountLink");
|
||||
|
||||
if (!$link) {
|
||||
return;
|
||||
}
|
||||
|
||||
$account = $this->user->getContact()?->getAccount();
|
||||
|
||||
if (!$account) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processPortalRecord($entity, $link, $account);
|
||||
}
|
||||
|
||||
private function processPortalContact(Entity $entity): void
|
||||
{
|
||||
/** @var ?string $link */
|
||||
$link = $this->metadata->get("aclDefs.{$entity->getEntityType()}.contactLink");
|
||||
|
||||
if (!$link) {
|
||||
return;
|
||||
}
|
||||
|
||||
$contact = $this->user->getContact();
|
||||
|
||||
if (!$contact) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processPortalRecord($entity, $link, $contact);
|
||||
}
|
||||
|
||||
private function processPortalRecord(Entity $entity, string $link, Account|Contact $record): void
|
||||
{
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fieldDefs = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entity->getEntityType())
|
||||
->tryGetField($link);
|
||||
|
||||
if (!$fieldDefs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
$fieldDefs->getType() === FieldType::LINK ||
|
||||
$fieldDefs->getType() === FieldType::LINK_ONE
|
||||
) {
|
||||
if ($entity->has($link . 'Id')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entity->setValueObject($link, Link::create($record->getId(), $record->getName()));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($fieldDefs->getType() === FieldType::LINK_MULTIPLE) {
|
||||
if ($entity->has($link . 'Ids')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$linkMultiple = LinkMultiple::create([LinkMultipleItem::create($record->getId(), $record->getName())]);
|
||||
|
||||
$entity->setValueObject($link, $linkMultiple);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
application/Espo/Core/Record/Defaults/Populator.php
Normal file
47
application/Espo/Core/Record/Defaults/Populator.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Defaults;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* Populate default values.
|
||||
*
|
||||
* @template TEntity of Entity
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
interface Populator
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function populate(Entity $entity): void;
|
||||
}
|
||||
78
application/Espo/Core/Record/Defaults/PopulatorFactory.php
Normal file
78
application/Espo/Core/Record/Defaults/PopulatorFactory.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?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\Record\Defaults;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class PopulatorFactory
|
||||
{
|
||||
/** @var class-string<DefaultPopulator> */
|
||||
private string $defaultClassName = DefaultPopulator::class;
|
||||
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private Metadata $metadata,
|
||||
private User $user,
|
||||
private Acl $acl
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return Populator<Entity>
|
||||
*/
|
||||
public function create(string $entityType): Populator
|
||||
{
|
||||
$binding = BindingContainerBuilder::create()
|
||||
->bindInstance(User::class, $this->user)
|
||||
->bindInstance(Acl::class, $this->acl)
|
||||
->build();
|
||||
|
||||
return $this->injectableFactory->createWithBinding($this->getClassName($entityType), $binding);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<Populator<Entity>>
|
||||
*/
|
||||
private function getClassName(string $entityType): string
|
||||
{
|
||||
/** @var ?class-string<Populator<Entity>> $className */
|
||||
$className = $this->metadata->get("recordDefs.$entityType.defaultsPopulatorClassName");
|
||||
|
||||
if ($className) {
|
||||
return $className;
|
||||
}
|
||||
|
||||
return $this->defaultClassName;
|
||||
}
|
||||
}
|
||||
43
application/Espo/Core/Record/DeleteParams.php
Normal file
43
application/Espo/Core/Record/DeleteParams.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record;
|
||||
|
||||
/**
|
||||
* Immutable.
|
||||
*/
|
||||
class DeleteParams
|
||||
{
|
||||
public function __construct() {}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
}
|
||||
42
application/Espo/Core/Record/DeleteParamsFetcher.php
Normal file
42
application/Espo/Core/Record/DeleteParamsFetcher.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
class DeleteParamsFetcher
|
||||
{
|
||||
public function __construct() {}
|
||||
|
||||
public function fetch(Request $request): DeleteParams
|
||||
{
|
||||
return DeleteParams::create();
|
||||
}
|
||||
}
|
||||
76
application/Espo/Core/Record/Deleted/DefaultRestorer.php
Normal file
76
application/Espo/Core/Record/Deleted/DefaultRestorer.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Deleted;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\ORM\Repository\Option\SaveOption;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
|
||||
/**
|
||||
* @implements Restorer<Entity>
|
||||
*/
|
||||
class DefaultRestorer implements Restorer
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Metadata $metadata,
|
||||
) {}
|
||||
|
||||
public function restore(Entity $entity): void
|
||||
{
|
||||
if (!$entity->get(Attribute::DELETED)) {
|
||||
throw new Forbidden("No 'deleted' attribute.");
|
||||
}
|
||||
|
||||
$this->entityManager
|
||||
->getTransactionManager()
|
||||
->run(fn () => $this->restoreInTransaction($entity));
|
||||
}
|
||||
|
||||
private function restoreInTransaction(Entity $entity): void
|
||||
{
|
||||
$repository = $this->entityManager->getRDBRepository($entity->getEntityType());
|
||||
|
||||
$repository->restoreDeleted($entity->getId());
|
||||
|
||||
if (
|
||||
$entity->hasAttribute('deleteId') &&
|
||||
$this->metadata->get("entityDefs.{$entity->getEntityType()}.deleteId")
|
||||
) {
|
||||
$this->entityManager->refreshEntity($entity);
|
||||
|
||||
$entity->set('deleteId', '0');
|
||||
$repository->save($entity, [SaveOption::SILENT => true]);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
application/Espo/Core/Record/Deleted/Restorer.php
Normal file
47
application/Espo/Core/Record/Deleted/Restorer.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Deleted;
|
||||
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface Restorer
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity A deleted entity.
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function restore(Entity $entity): void;
|
||||
}
|
||||
111
application/Espo/Core/Record/Duplicator/EntityDuplicator.php
Normal file
111
application/Espo/Core/Record/Duplicator/EntityDuplicator.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?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\Record\Duplicator;
|
||||
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Defs\FieldDefs;
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Duplicates an entity.
|
||||
*/
|
||||
class EntityDuplicator
|
||||
{
|
||||
public function __construct(
|
||||
private Defs $defs,
|
||||
private FieldDuplicatorFactory $fieldDuplicatorFactory,
|
||||
private FieldUtil $fieldUtil,
|
||||
private Metadata $metadata
|
||||
) {}
|
||||
|
||||
public function duplicate(Entity $entity): stdClass
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
$valueMap = $entity->getValueMap();
|
||||
|
||||
unset($valueMap->id);
|
||||
|
||||
$entityDefs = $this->defs->getEntity($entityType);
|
||||
|
||||
foreach ($entityDefs->getFieldList() as $fieldDefs) {
|
||||
$this->processField($entity, $fieldDefs, $valueMap);
|
||||
}
|
||||
|
||||
return $valueMap;
|
||||
}
|
||||
|
||||
private function processField(Entity $entity, FieldDefs $fieldDefs, stdClass $valueMap): void
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
$field = $fieldDefs->getName();
|
||||
|
||||
if ($this->toIgnoreField($entityType, $fieldDefs)) {
|
||||
$attributeList = $this->fieldUtil->getAttributeList($entityType, $field);
|
||||
|
||||
foreach ($attributeList as $attribute) {
|
||||
unset($valueMap->$attribute);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->fieldDuplicatorFactory->has($entityType, $field)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fieldDuplicator = $this->fieldDuplicatorFactory->create($entityType, $field);
|
||||
|
||||
$fieldValueMap = $fieldDuplicator->duplicate($entity, $field);
|
||||
|
||||
foreach (get_object_vars($fieldValueMap) as $attribute => $value) {
|
||||
$valueMap->$attribute = $value;
|
||||
}
|
||||
}
|
||||
|
||||
private function toIgnoreField(string $entityType, FieldDefs $fieldDefs): bool
|
||||
{
|
||||
$type = $fieldDefs->getType();
|
||||
|
||||
if (in_array($type, [FieldType::AUTOINCREMENT, FieldType::NUMBER])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->metadata->get(['scopes', $entityType, 'statusField']) === $fieldDefs->getName()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (bool) $fieldDefs->getParam('duplicateIgnore');
|
||||
}
|
||||
}
|
||||
43
application/Espo/Core/Record/Duplicator/FieldDuplicator.php
Normal file
43
application/Espo/Core/Record/Duplicator/FieldDuplicator.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Duplicator;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Duplicates attributes of a field. Some fields can require some processing
|
||||
* when an entity is being duplicated.
|
||||
*/
|
||||
interface FieldDuplicator
|
||||
{
|
||||
public function duplicate(Entity $entity, string $field): stdClass;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Duplicator;
|
||||
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\InjectableFactory;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class FieldDuplicatorFactory
|
||||
{
|
||||
public function __construct(
|
||||
private Defs $defs,
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
public function create(string $entityType, string $field): FieldDuplicator
|
||||
{
|
||||
$className = $this->getClassName($entityType, $field);
|
||||
|
||||
if (!$className) {
|
||||
throw new RuntimeException("No field duplicator for the field.");
|
||||
}
|
||||
|
||||
return $this->injectableFactory->create($className);
|
||||
}
|
||||
|
||||
public function has(string $entityType, string $field): bool
|
||||
{
|
||||
return $this->getClassName($entityType, $field) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?class-string<FieldDuplicator>
|
||||
*/
|
||||
private function getClassName(string $entityType, string $field): ?string
|
||||
{
|
||||
$fieldDefs = $this->defs
|
||||
->getEntity($entityType)
|
||||
->getField($field);
|
||||
|
||||
$className1 = $fieldDefs->getParam('duplicatorClassName');
|
||||
|
||||
if ($className1) {
|
||||
/** @var class-string<FieldDuplicator> */
|
||||
return $className1;
|
||||
}
|
||||
|
||||
$type = $fieldDefs->getType();
|
||||
|
||||
$className2 = $this->metadata->get(['fields', $type, 'duplicatorClassName']);
|
||||
|
||||
if ($className2) {
|
||||
/** @var class-string<FieldDuplicator> */
|
||||
return $className2;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\DynamicLogic;
|
||||
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Tools\DynamicLogic\ConditionChecker;
|
||||
use Espo\Tools\DynamicLogic\ConditionCheckerFactory;
|
||||
use Espo\Tools\DynamicLogic\Exceptions\BadCondition;
|
||||
use Espo\Tools\DynamicLogic\Item;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
|
||||
class InputFilterProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private FieldUtil $fieldUtil,
|
||||
private ConditionCheckerFactory $conditionCheckerFactory,
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity, stdClass $input): void
|
||||
{
|
||||
/** @var array<string, array<string, mixed>> $fieldsDefs */
|
||||
$fieldsDefs = $this->metadata->get("logicDefs.{$entity->getEntityType()}.fields") ?? [];
|
||||
|
||||
$checker = null;
|
||||
|
||||
foreach ($fieldsDefs as $field => $defs) {
|
||||
if ($defs['readOnlySaved'] ?? null) {
|
||||
$checker ??= $this->conditionCheckerFactory->create($entity);
|
||||
|
||||
$this->processField($entity, $input, $field, $checker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function processField(Entity $entity, stdClass $input, string $field, ConditionChecker $checker): void
|
||||
{
|
||||
/** @var ?stdClass[] $group */
|
||||
$group = $this->metadata
|
||||
->getObjects("logicDefs.{$entity->getEntityType()}.fields.$field.readOnlySaved.conditionGroup");
|
||||
|
||||
if (!$group) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$item = Item::fromGroupDefinition($group);
|
||||
|
||||
if (!$checker->check($item)) {
|
||||
return;
|
||||
}
|
||||
} catch (BadCondition $e) {
|
||||
throw new RuntimeException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
foreach ($this->fieldUtil->getAttributeList($entity->getEntityType(), $field) as $attribute) {
|
||||
unset($input->$attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
105
application/Espo/Core/Record/EntityProvider.php
Normal file
105
application/Espo/Core/Record/EntityProvider.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?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\Record;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* Fetches entities.
|
||||
*
|
||||
* @since 8.1.0
|
||||
*/
|
||||
class EntityProvider
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Fetch an entity.
|
||||
*
|
||||
* @template T of Entity
|
||||
* @param class-string<T> $className An entity class name.
|
||||
* @return T
|
||||
* @throws NotFound A record not found.
|
||||
* @throws Forbidden Read is forbidden for a current user.
|
||||
* @since 8.3.0
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function getByClass(string $className, string $id): Entity
|
||||
{
|
||||
$entity = $this->entityManager
|
||||
->getRDBRepositoryByClass($className)
|
||||
->getById($id);
|
||||
|
||||
return $this->processGet($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an entity by an entity type.
|
||||
*
|
||||
* @return Entity
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @since 9.0.0
|
||||
*/
|
||||
public function get(string $entityType, string $id): Entity
|
||||
{
|
||||
$entity = $this->entityManager->getEntityById($entityType, $id);
|
||||
|
||||
return $this->processGet($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of Entity
|
||||
* @param ?T $entity
|
||||
* @return T
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
private function processGet(?Entity $entity): Entity
|
||||
{
|
||||
if (!$entity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$this->acl->checkEntityRead($entity)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
58
application/Espo/Core/Record/FindParams.php
Normal file
58
application/Espo/Core/Record/FindParams.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?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\Record;
|
||||
|
||||
/**
|
||||
* Immutable.
|
||||
*/
|
||||
class FindParams
|
||||
{
|
||||
private bool $noTotal = false;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function withNoTotal(bool $noTotal = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->noTotal = $noTotal;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function noTotal(): bool
|
||||
{
|
||||
return $this->noTotal;
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
}
|
||||
49
application/Espo/Core/Record/FindParamsFetcher.php
Normal file
49
application/Espo/Core/Record/FindParamsFetcher.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?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\Record;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
class FindParamsFetcher
|
||||
{
|
||||
public function __construct() {}
|
||||
|
||||
public function fetch(Request $request): FindParams
|
||||
{
|
||||
$noTotal = strtolower($request->getHeader('X-No-Total') ?? '') === 'true';
|
||||
|
||||
if ($request->getQueryParam('q')) {
|
||||
$noTotal = true;
|
||||
}
|
||||
|
||||
return FindParams::create()
|
||||
->withNoTotal($noTotal);
|
||||
}
|
||||
}
|
||||
103
application/Espo/Core/Record/Formula/Processor.php
Normal file
103
application/Espo/Core/Record/Formula/Processor.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Formula;
|
||||
|
||||
use Espo\Core\Formula\Exceptions\Error as FormulaError;
|
||||
use Espo\Core\Formula\Manager as FormulaManager;
|
||||
use Espo\Core\Record\CreateParams;
|
||||
use Espo\Core\Record\UpdateParams;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Formula script processing for API requests.
|
||||
*/
|
||||
class Processor
|
||||
{
|
||||
public function __construct(
|
||||
private FormulaManager $formulaManager,
|
||||
private Metadata $metadata
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Process a before-create formula script.
|
||||
*/
|
||||
public function processBeforeCreate(Entity $entity, CreateParams $params): void
|
||||
{
|
||||
$script = $this->getScript($entity->getEntityType());
|
||||
|
||||
if (!$script) {
|
||||
return;
|
||||
}
|
||||
|
||||
$variables = (object) [
|
||||
'__skipDuplicateCheck' => $params->skipDuplicateCheck(),
|
||||
'__isRecordService' => true,
|
||||
];
|
||||
|
||||
$this->run($script, $entity, $variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a before-update formula script.
|
||||
*/
|
||||
public function processBeforeUpdate(Entity $entity, UpdateParams $params): void
|
||||
{
|
||||
$script = $this->getScript($entity->getEntityType());
|
||||
|
||||
if (!$script) {
|
||||
return;
|
||||
}
|
||||
|
||||
$variables = (object) [
|
||||
'__skipDuplicateCheck' => $params->skipDuplicateCheck(),
|
||||
'__isRecordService' => true,
|
||||
];
|
||||
|
||||
$this->run($script, $entity, $variables);
|
||||
}
|
||||
|
||||
private function run(string $script, Entity $entity, stdClass $variables): void
|
||||
{
|
||||
try {
|
||||
$this->formulaManager->run($script, $entity, $variables);
|
||||
} catch (FormulaError $e) {
|
||||
throw new RuntimeException('Formula script error.', 500, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function getScript(string $entityType): ?string
|
||||
{
|
||||
/** @var ?string */
|
||||
return $this->metadata->get(['formula', $entityType, 'beforeSaveApiScript']);
|
||||
}
|
||||
}
|
||||
50
application/Espo/Core/Record/Hook/CreateHook.php
Normal file
50
application/Espo/Core/Record/Hook/CreateHook.php
Normal 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\Record\Hook;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Record\CreateParams;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface CreateHook
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function process(Entity $entity, CreateParams $params): void;
|
||||
}
|
||||
50
application/Espo/Core/Record/Hook/DeleteHook.php
Normal file
50
application/Espo/Core/Record/Hook/DeleteHook.php
Normal 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\Record\Hook;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Record\DeleteParams;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface DeleteHook
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function process(Entity $entity, DeleteParams $params): void;
|
||||
}
|
||||
43
application/Espo/Core/Record/Hook/LinkHook.php
Normal file
43
application/Espo/Core/Record/Hook/LinkHook.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Hook;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface LinkHook
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
*/
|
||||
public function process(Entity $entity, string $link, Entity $foreignEntity): void;
|
||||
}
|
||||
132
application/Espo/Core/Record/Hook/Provider.php
Normal file
132
application/Espo/Core/Record/Hook/Provider.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?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\Record\Hook;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Binding\BindingContainer;
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\InjectableFactory;
|
||||
|
||||
use Espo\Entities\User;
|
||||
use ReflectionClass;
|
||||
use RuntimeException;
|
||||
|
||||
class Provider
|
||||
{
|
||||
/** @var array<string, object[]> */
|
||||
private $map = [];
|
||||
|
||||
/** @var array<string, class-string[]> */
|
||||
private $typeInterfaceListMap = [
|
||||
Type::BEFORE_READ => [ReadHook::class],
|
||||
Type::EARLY_BEFORE_CREATE => [CreateHook::class, SaveHook::class],
|
||||
Type::BEFORE_CREATE => [CreateHook::class, SaveHook::class],
|
||||
Type::AFTER_CREATE => [CreateHook::class, SaveHook::class],
|
||||
Type::EARLY_BEFORE_UPDATE => [UpdateHook::class, SaveHook::class],
|
||||
Type::BEFORE_UPDATE => [UpdateHook::class, SaveHook::class],
|
||||
Type::AFTER_UPDATE => [UpdateHook::class, SaveHook::class],
|
||||
Type::BEFORE_DELETE => [DeleteHook::class],
|
||||
Type::AFTER_DELETE => [DeleteHook::class],
|
||||
Type::BEFORE_LINK => [LinkHook::class],
|
||||
Type::BEFORE_UNLINK => [UnlinkHook::class],
|
||||
Type::AFTER_LINK => [LinkHook::class],
|
||||
Type::AFTER_UNLINK => [UnlinkHook::class],
|
||||
];
|
||||
|
||||
private BindingContainer $bindingContainer;
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private Acl $acl,
|
||||
private User $user
|
||||
) {
|
||||
$this->bindingContainer = BindingContainerBuilder::create()
|
||||
->bindInstance(User::class, $this->user)
|
||||
->bindInstance(Acl::class, $this->acl)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return object[]
|
||||
*/
|
||||
public function getList(string $entityType, string $type): array
|
||||
{
|
||||
$key = $entityType . '_' . $type;
|
||||
|
||||
if (!array_key_exists($key, $this->map)) {
|
||||
$this->map[$key] = $this->loadList($entityType, $type);
|
||||
}
|
||||
|
||||
return $this->map[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return object[]
|
||||
*/
|
||||
private function loadList(string $entityType, string $type): array
|
||||
{
|
||||
$key = $type . 'HookClassNameList';
|
||||
|
||||
/** @var class-string[] $classNameList */
|
||||
$classNameList = $this->metadata->get(['recordDefs', $entityType, $key]) ?? [];
|
||||
|
||||
$interfaces = $this->typeInterfaceListMap[$type] ?? null;
|
||||
|
||||
if (!$interfaces) {
|
||||
throw new RuntimeException("Unsupported record hook type '$type'.");
|
||||
}
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach ($classNameList as $className) {
|
||||
$class = new ReflectionClass($className);
|
||||
|
||||
$found = false;
|
||||
|
||||
foreach ($interfaces as $interface) {
|
||||
if ($class->implementsInterface($interface)) {
|
||||
$found = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
throw new RuntimeException("Hook '$className' does not implement any required interface.");
|
||||
}
|
||||
|
||||
$list[] = $this->injectableFactory->createWithBinding($className, $this->bindingContainer);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
44
application/Espo/Core/Record/Hook/ReadHook.php
Normal file
44
application/Espo/Core/Record/Hook/ReadHook.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Hook;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Record\ReadParams;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface ReadHook
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
*/
|
||||
public function process(Entity $entity, ReadParams $params): void;
|
||||
}
|
||||
52
application/Espo/Core/Record/Hook/SaveHook.php
Normal file
52
application/Espo/Core/Record/Hook/SaveHook.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?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\Record\Hook;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* On record create or update.
|
||||
*
|
||||
* @template TEntity of Entity
|
||||
* @since 8.1.0.
|
||||
*/
|
||||
interface SaveHook
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function process(Entity $entity): void;
|
||||
}
|
||||
51
application/Espo/Core/Record/Hook/Type.php
Normal file
51
application/Espo/Core/Record/Hook/Type.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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\Record\Hook;
|
||||
|
||||
class Type
|
||||
{
|
||||
public const BEFORE_READ = 'beforeRead';
|
||||
|
||||
public const EARLY_BEFORE_CREATE = 'earlyBeforeCreate';
|
||||
public const EARLY_BEFORE_UPDATE = 'earlyBeforeUpdate';
|
||||
|
||||
public const BEFORE_CREATE = 'beforeCreate';
|
||||
public const BEFORE_UPDATE = 'beforeUpdate';
|
||||
public const BEFORE_DELETE = 'beforeDelete';
|
||||
|
||||
public const AFTER_CREATE = 'afterCreate';
|
||||
public const AFTER_UPDATE = 'afterUpdate';
|
||||
public const AFTER_DELETE = 'afterDelete';
|
||||
|
||||
public const BEFORE_LINK = 'beforeLink';
|
||||
public const BEFORE_UNLINK = 'beforeUnlink';
|
||||
public const AFTER_LINK = 'afterLink';
|
||||
public const AFTER_UNLINK = 'afterUnlink';
|
||||
}
|
||||
43
application/Espo/Core/Record/Hook/UnlinkHook.php
Normal file
43
application/Espo/Core/Record/Hook/UnlinkHook.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Hook;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface UnlinkHook
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
*/
|
||||
public function process(Entity $entity, string $link, Entity $foreignEntity): void;
|
||||
}
|
||||
51
application/Espo/Core/Record/Hook/UpdateHook.php
Normal file
51
application/Espo/Core/Record/Hook/UpdateHook.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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\Record\Hook;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Core\Record\UpdateParams;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface UpdateHook
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function process(Entity $entity, UpdateParams $params): void;
|
||||
}
|
||||
334
application/Espo/Core/Record/HookManager.php
Normal file
334
application/Espo/Core/Record/HookManager.php
Normal file
@@ -0,0 +1,334 @@
|
||||
<?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\Record;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Record\Hook\CreateHook;
|
||||
use Espo\Core\Record\Hook\DeleteHook;
|
||||
use Espo\Core\Record\Hook\LinkHook;
|
||||
use Espo\Core\Record\Hook\ReadHook;
|
||||
use Espo\Core\Record\Hook\SaveHook;
|
||||
use Espo\Core\Record\Hook\UnlinkHook;
|
||||
use Espo\Core\Record\Hook\UpdateHook;
|
||||
use Espo\Core\Record\Hook\Provider;
|
||||
use Espo\Core\Record\Hook\Type;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class HookManager
|
||||
{
|
||||
public function __construct(private Provider $provider)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function processEarlyBeforeCreate(Entity $entity, CreateParams $params): void
|
||||
{
|
||||
foreach ($this->getEarlyBeforeCreateHookList($entity->getEntityType()) as $hook) {
|
||||
if ($hook instanceof SaveHook) {
|
||||
$hook->process($entity);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$hook->process($entity, $params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function processBeforeCreate(Entity $entity, CreateParams $params): void
|
||||
{
|
||||
foreach ($this->getBeforeCreateHookList($entity->getEntityType()) as $hook) {
|
||||
if ($hook instanceof SaveHook) {
|
||||
$hook->process($entity);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$hook->process($entity, $params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function processAfterCreate(Entity $entity, CreateParams $params): void
|
||||
{
|
||||
foreach ($this->getAfterCreateHookList($entity->getEntityType()) as $hook) {
|
||||
if ($hook instanceof SaveHook) {
|
||||
$hook->process($entity);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$hook->process($entity, $params);
|
||||
}
|
||||
}
|
||||
|
||||
public function processBeforeRead(Entity $entity, ReadParams $params): void
|
||||
{
|
||||
foreach ($this->getBeforeReadHookList($entity->getEntityType()) as $hook) {
|
||||
$hook->process($entity, $params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function processEarlyBeforeUpdate(Entity $entity, UpdateParams $params): void
|
||||
{
|
||||
foreach ($this->getEarlyBeforeUpdateHookList($entity->getEntityType()) as $hook) {
|
||||
if ($hook instanceof SaveHook) {
|
||||
$hook->process($entity);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$hook->process($entity, $params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function processBeforeUpdate(Entity $entity, UpdateParams $params): void
|
||||
{
|
||||
foreach ($this->getBeforeUpdateHookList($entity->getEntityType()) as $hook) {
|
||||
if ($hook instanceof SaveHook) {
|
||||
$hook->process($entity);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$hook->process($entity, $params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function processAfterUpdate(Entity $entity, UpdateParams $params): void
|
||||
{
|
||||
foreach ($this->getAfterUpdateHookList($entity->getEntityType()) as $hook) {
|
||||
if ($hook instanceof SaveHook) {
|
||||
$hook->process($entity);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$hook->process($entity, $params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function processBeforeDelete(Entity $entity, DeleteParams $params): void
|
||||
{
|
||||
foreach ($this->getBeforeDeleteHookList($entity->getEntityType()) as $hook) {
|
||||
$hook->process($entity, $params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function processAfterDelete(Entity $entity, DeleteParams $params): void
|
||||
{
|
||||
foreach ($this->getAfterDeleteHookList($entity->getEntityType()) as $hook) {
|
||||
$hook->process($entity, $params);
|
||||
}
|
||||
}
|
||||
|
||||
public function processBeforeLink(Entity $entity, string $link, Entity $foreignEntity): void
|
||||
{
|
||||
foreach ($this->getBeforeLinkHookList($entity->getEntityType()) as $hook) {
|
||||
$hook->process($entity, $link, $foreignEntity);
|
||||
}
|
||||
}
|
||||
|
||||
public function processBeforeUnlink(Entity $entity, string $link, Entity $foreignEntity): void
|
||||
{
|
||||
foreach ($this->getBeforeUnlinkHookList($entity->getEntityType()) as $hook) {
|
||||
$hook->process($entity, $link, $foreignEntity);
|
||||
}
|
||||
}
|
||||
|
||||
public function processAfterLink(Entity $entity, string $link, Entity $foreignEntity): void
|
||||
{
|
||||
foreach ($this->getAfterLinkHookList($entity->getEntityType()) as $hook) {
|
||||
$hook->process($entity, $link, $foreignEntity);
|
||||
}
|
||||
}
|
||||
|
||||
public function processAfterUnlink(Entity $entity, string $link, Entity $foreignEntity): void
|
||||
{
|
||||
foreach ($this->getAfterUnlinkHookList($entity->getEntityType()) as $hook) {
|
||||
$hook->process($entity, $link, $foreignEntity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ReadHook<Entity>[]
|
||||
*/
|
||||
private function getBeforeReadHookList(string $entityType): array
|
||||
{
|
||||
/** @var ReadHook<Entity>[] */
|
||||
return $this->provider->getList($entityType, Type::BEFORE_READ);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return (CreateHook<Entity>|SaveHook<Entity>)[]
|
||||
*/
|
||||
private function getEarlyBeforeCreateHookList(string $entityType): array
|
||||
{
|
||||
/** @var (CreateHook<Entity>|SaveHook<Entity>)[] */
|
||||
return $this->provider->getList($entityType, Type::EARLY_BEFORE_CREATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return (CreateHook<Entity>|SaveHook<Entity>)[]
|
||||
*/
|
||||
private function getBeforeCreateHookList(string $entityType): array
|
||||
{
|
||||
/** @var (CreateHook<Entity>|SaveHook<Entity>)[] */
|
||||
return $this->provider->getList($entityType, Type::BEFORE_CREATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return (CreateHook<Entity>|SaveHook<Entity>)[]
|
||||
*/
|
||||
private function getAfterCreateHookList(string $entityType): array
|
||||
{
|
||||
/** @var (CreateHook<Entity>|SaveHook<Entity>)[] */
|
||||
return $this->provider->getList($entityType, Type::AFTER_CREATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return (UpdateHook<Entity>|SaveHook<Entity>)[]
|
||||
*/
|
||||
private function getEarlyBeforeUpdateHookList(string $entityType): array
|
||||
{
|
||||
/** @var (UpdateHook<Entity>|SaveHook<Entity>)[] */
|
||||
return $this->provider->getList($entityType, Type::EARLY_BEFORE_UPDATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return (UpdateHook<Entity>|SaveHook<Entity>)[]
|
||||
*/
|
||||
private function getBeforeUpdateHookList(string $entityType): array
|
||||
{
|
||||
/** @var (UpdateHook<Entity>|SaveHook<Entity>)[] */
|
||||
return $this->provider->getList($entityType, Type::BEFORE_UPDATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return (UpdateHook<Entity>|SaveHook<Entity>)[]
|
||||
*/
|
||||
private function getAfterUpdateHookList(string $entityType): array
|
||||
{
|
||||
/** @var (UpdateHook<Entity>|SaveHook<Entity>)[] */
|
||||
return $this->provider->getList($entityType, Type::AFTER_UPDATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DeleteHook<Entity>[]
|
||||
*/
|
||||
private function getBeforeDeleteHookList(string $entityType): array
|
||||
{
|
||||
/** @var DeleteHook<Entity>[] */
|
||||
return $this->provider->getList($entityType, Type::BEFORE_DELETE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DeleteHook<Entity>[]
|
||||
*/
|
||||
private function getAfterDeleteHookList(string $entityType): array
|
||||
{
|
||||
/** @var DeleteHook<Entity>[] */
|
||||
return $this->provider->getList($entityType, Type::AFTER_DELETE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LinkHook<Entity>[]
|
||||
*/
|
||||
private function getBeforeLinkHookList(string $entityType): array
|
||||
{
|
||||
/** @var LinkHook<Entity>[] */
|
||||
return $this->provider->getList($entityType, Type::BEFORE_LINK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UnlinkHook<Entity>[]
|
||||
*/
|
||||
private function getBeforeUnlinkHookList(string $entityType): array
|
||||
{
|
||||
/** @var UnlinkHook<Entity>[] */
|
||||
return $this->provider->getList($entityType, Type::BEFORE_UNLINK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LinkHook<Entity>[]
|
||||
*/
|
||||
private function getAfterLinkHookList(string $entityType): array
|
||||
{
|
||||
/** @var LinkHook<Entity>[] */
|
||||
return $this->provider->getList($entityType, Type::AFTER_LINK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UnlinkHook<Entity>[]
|
||||
*/
|
||||
private function getAfterUnlinkHookList(string $entityType): array
|
||||
{
|
||||
/** @var UnlinkHook<Entity>[] */
|
||||
return $this->provider->getList($entityType, Type::AFTER_UNLINK);
|
||||
}
|
||||
}
|
||||
90
application/Espo/Core/Record/Input/Data.php
Normal file
90
application/Espo/Core/Record/Input/Data.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Input;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class Data
|
||||
{
|
||||
public function __construct(private stdClass $raw) {}
|
||||
|
||||
/**
|
||||
* Get all attributes.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAttributeList(): array
|
||||
{
|
||||
return array_keys(get_object_vars($this->raw));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset an attribute.
|
||||
*
|
||||
* @param string $name An attribute name.
|
||||
*/
|
||||
public function clear(string $name): self
|
||||
{
|
||||
unset($this->raw->$name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an attribute is set.
|
||||
*
|
||||
* @param string $name An attribute name.
|
||||
*/
|
||||
public function has(string $name): bool
|
||||
{
|
||||
return property_exists($this->raw, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an attribute value.
|
||||
*
|
||||
* @param string $name An attribute name.
|
||||
*/
|
||||
public function get(string $name): mixed
|
||||
{
|
||||
return $this->raw->$name ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an attribute value.
|
||||
*
|
||||
* @param string $name An attribute name.
|
||||
* @param mixed $value A value
|
||||
*/
|
||||
public function set(string $name, mixed $value): mixed
|
||||
{
|
||||
return $this->raw->$name = $value;
|
||||
}
|
||||
}
|
||||
38
application/Espo/Core/Record/Input/Filter.php
Normal file
38
application/Espo/Core/Record/Input/Filter.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Input;
|
||||
|
||||
/**
|
||||
* An input filter.
|
||||
*/
|
||||
interface Filter
|
||||
{
|
||||
public function filter(Data $data): void;
|
||||
}
|
||||
100
application/Espo/Core/Record/Input/FilterProvider.php
Normal file
100
application/Espo/Core/Record/Input/FilterProvider.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?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\Record\Input;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
|
||||
class FilterProvider
|
||||
{
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private Metadata $metadata,
|
||||
private Acl $acl,
|
||||
private User $user
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return Filter[]
|
||||
*/
|
||||
public function getForCreate(string $entityType): array
|
||||
{
|
||||
$classNameList = $this->getCreateClassNameList($entityType);
|
||||
|
||||
$binding = BindingContainerBuilder::create()
|
||||
->bindInstance(User::class, $this->user)
|
||||
->bindInstance(Acl::class, $this->acl)
|
||||
->build();
|
||||
|
||||
return array_map(
|
||||
fn ($className) => $this->injectableFactory->createWithBinding($className, $binding),
|
||||
$classNameList
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Filter[]
|
||||
*/
|
||||
public function getForUpdate(string $entityType): array
|
||||
{
|
||||
$classNameList = $this->getUpdateClassNameList($entityType);
|
||||
|
||||
$binding = BindingContainerBuilder::create()
|
||||
->bindInstance(User::class, $this->user)
|
||||
->bindInstance(Acl::class, $this->acl)
|
||||
->build();
|
||||
|
||||
return array_map(
|
||||
fn ($className) => $this->injectableFactory->createWithBinding($className, $binding),
|
||||
$classNameList
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<Filter>[]
|
||||
*/
|
||||
private function getCreateClassNameList(string $entityType): array
|
||||
{
|
||||
/** @var class-string<Filter>[] */
|
||||
return $this->metadata->get("recordDefs.$entityType.createInputFilterClassNameList") ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<Filter>[]
|
||||
*/
|
||||
private function getUpdateClassNameList(string $entityType): array
|
||||
{
|
||||
/** @var class-string<Filter>[] */
|
||||
return $this->metadata->get("recordDefs.$entityType.updateInputFilterClassNameList") ?? [];
|
||||
}
|
||||
}
|
||||
45
application/Espo/Core/Record/Output/Filter.php
Normal file
45
application/Espo/Core/Record/Output/Filter.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Output;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* Filters entity attribute values for output.
|
||||
*
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface Filter
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
*/
|
||||
public function filter(Entity $entity): void;
|
||||
}
|
||||
74
application/Espo/Core/Record/Output/FilterProvider.php
Normal file
74
application/Espo/Core/Record/Output/FilterProvider.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Output;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class FilterProvider
|
||||
{
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private Metadata $metadata,
|
||||
private Acl $acl,
|
||||
private User $user
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return Filter<Entity>[]
|
||||
*/
|
||||
public function get(string $entityType): array
|
||||
{
|
||||
$classNameList = $this->getClassNameList($entityType);
|
||||
|
||||
$binding = BindingContainerBuilder::create()
|
||||
->bindInstance(User::class, $this->user)
|
||||
->bindInstance(Acl::class, $this->acl)
|
||||
->build();
|
||||
|
||||
return array_map(
|
||||
fn ($className) => $this->injectableFactory->createWithBinding($className, $binding),
|
||||
$classNameList
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<Filter<Entity>>[]
|
||||
*/
|
||||
private function getClassNameList(string $entityType): array
|
||||
{
|
||||
/** @var class-string<Filter<Entity>>[] */
|
||||
return $this->metadata->get("recordDefs.$entityType.outputFilterClassNameList") ?? [];
|
||||
}
|
||||
}
|
||||
43
application/Espo/Core/Record/ReadParams.php
Normal file
43
application/Espo/Core/Record/ReadParams.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record;
|
||||
|
||||
/**
|
||||
* Immutable.
|
||||
*/
|
||||
class ReadParams
|
||||
{
|
||||
public function __construct() {}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
}
|
||||
42
application/Espo/Core/Record/ReadParamsFetcher.php
Normal file
42
application/Espo/Core/Record/ReadParamsFetcher.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
class ReadParamsFetcher
|
||||
{
|
||||
public function __construct() {}
|
||||
|
||||
public function fetch(Request $request): ReadParams
|
||||
{
|
||||
return ReadParams::create();
|
||||
}
|
||||
}
|
||||
255
application/Espo/Core/Record/SearchParamsFetcher.php
Normal file
255
application/Espo/Core/Record/SearchParamsFetcher.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?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\Record;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Select\SearchParams;
|
||||
use Espo\Core\Select\Text\MetadataProvider as TextMetadataProvider;
|
||||
use Espo\Core\Utils\Json;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use JsonException;
|
||||
|
||||
class SearchParamsFetcher
|
||||
{
|
||||
private const MAX_SIZE_LIMIT = 200;
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private TextMetadataProvider $textMetadataProvider
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Fetch search params from a request.
|
||||
*
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function fetch(Request $request): SearchParams
|
||||
{
|
||||
try {
|
||||
return SearchParams::fromRaw($this->fetchRaw($request));
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new BadRequest($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function fetchRaw(Request $request): array
|
||||
{
|
||||
$params = $request->hasQueryParam('searchParams') ?
|
||||
$this->fetchRawJsonSearchParams($request):
|
||||
$this->fetchRawMultipleParams($request);
|
||||
|
||||
$this->handleRawParams($params, $request);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function fetchRawJsonSearchParams(Request $request): array
|
||||
{
|
||||
try {
|
||||
return Json::decode($request->getQueryParam('searchParams') ?? '', true);
|
||||
} catch (JsonException) {
|
||||
throw new BadRequest("Invalid search params JSON.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function fetchRawMultipleParams(Request $request): array
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
$params['where'] = $queryParams['whereGroup'] ?? $queryParams['where'] ?? null;
|
||||
|
||||
if ($params['where'] !== null && !is_array($params['where'])) {
|
||||
$params['where'] = null;
|
||||
}
|
||||
|
||||
$params['maxSize'] = $request->getQueryParam('maxSize');
|
||||
$params['offset'] = $request->getQueryParam('offset');
|
||||
|
||||
if ($params['maxSize'] === '') {
|
||||
$params['maxSize'] = null;
|
||||
}
|
||||
|
||||
if ($params['offset'] === '') {
|
||||
$params['offset'] = null;
|
||||
}
|
||||
|
||||
if ($params['maxSize'] !== null) {
|
||||
$params['maxSize'] = intval($params['maxSize']);
|
||||
}
|
||||
|
||||
if ($params['offset'] !== null) {
|
||||
$params['offset'] = intval($params['offset']);
|
||||
}
|
||||
|
||||
if ($request->getQueryParam('orderBy')) {
|
||||
$params['orderBy'] = $request->getQueryParam('orderBy');
|
||||
} else if ($request->getQueryParam('sortBy')) {
|
||||
// legacy
|
||||
$params['orderBy'] = $request->getQueryParam('sortBy');
|
||||
}
|
||||
|
||||
if ($request->getQueryParam('order')) {
|
||||
$params['order'] = strtoupper($request->getQueryParam('order'));
|
||||
} else if ($request->getQueryParam('asc')) {
|
||||
// legacy
|
||||
$params['order'] = $request->getQueryParam('asc') === 'true' ?
|
||||
SearchParams::ORDER_ASC : SearchParams::ORDER_DESC;
|
||||
}
|
||||
|
||||
$q = $request->getQueryParam('q');
|
||||
|
||||
if ($q) {
|
||||
$params['q'] = trim($q);
|
||||
}
|
||||
|
||||
if ($request->getQueryParam('textFilter')) {
|
||||
$params['textFilter'] = $request->getQueryParam('textFilter');
|
||||
}
|
||||
|
||||
if ($request->getQueryParam('primaryFilter')) {
|
||||
$params['primaryFilter'] = $request->getQueryParam('primaryFilter');
|
||||
}
|
||||
|
||||
if ($queryParams['boolFilterList'] ?? null) {
|
||||
$params['boolFilterList'] = (array) $queryParams['boolFilterList'];
|
||||
}
|
||||
|
||||
if ($queryParams['filterList'] ?? null) {
|
||||
$params['filterList'] = (array) $queryParams['filterList'];
|
||||
}
|
||||
|
||||
$select = $request->getQueryParam('attributeSelect') ?? $request->getQueryParam('select');
|
||||
|
||||
if ($select) {
|
||||
$params['select'] = explode(',', $select);
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function handleRawParams(array &$params, Request $request): void
|
||||
{
|
||||
if (isset($params['maxSize']) && !is_int($params['maxSize'])) {
|
||||
throw new BadRequest('maxSize must be integer.');
|
||||
}
|
||||
|
||||
$this->handleQ($params, $request);
|
||||
$this->handleMaxSize($params);
|
||||
}
|
||||
|
||||
private function hasFullTextSearch(Request $request): bool
|
||||
{
|
||||
$scope = $request->getRouteParam('controller');
|
||||
|
||||
if (!$scope) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($request->getRouteParam('action') !== 'index') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->textMetadataProvider->hasFullTextSearch($scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function handleMaxSize(array &$params): void
|
||||
{
|
||||
$value = $params['maxSize'] ?? null;
|
||||
|
||||
$limit = $this->config->get('recordListMaxSizeLimit') ?? self::MAX_SIZE_LIMIT;
|
||||
|
||||
if ($value === null) {
|
||||
$params['maxSize'] = $limit;
|
||||
}
|
||||
|
||||
if ($value > $limit) {
|
||||
throw new Forbidden("Max size should not exceed $limit. Use offset and limit.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function handleQ(array &$params, Request $request): void
|
||||
{
|
||||
$q = $params['q'] ?? null;
|
||||
|
||||
if ($q === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_string($q)) {
|
||||
throw new BadRequest("q must be string.");
|
||||
}
|
||||
|
||||
if (!$this->config->get('quickSearchFullTextAppendWildcard')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!str_contains($q, '*') &&
|
||||
!str_contains($q, '"') &&
|
||||
!str_contains($q, '+') &&
|
||||
!str_contains($q, '-') &&
|
||||
$this->hasFullTextSearch($request)
|
||||
) {
|
||||
$params['q'] = $q . '*';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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\Record\Select;
|
||||
|
||||
use Espo\Core\Select\Applier\AdditionalApplier;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
class ApplierClassNameListProvider
|
||||
{
|
||||
public function __construct(private Metadata $metadata)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @return class-string<AdditionalApplier>[]
|
||||
*/
|
||||
public function get(string $entityType): array
|
||||
{
|
||||
return [
|
||||
...$this->metadata->get("app.record.selectApplierClassNameList", []),
|
||||
...$this->metadata->get("recordDefs.$entityType.selectApplierClassNameList", []),
|
||||
];
|
||||
}
|
||||
}
|
||||
1797
application/Espo/Core/Record/Service.php
Normal file
1797
application/Espo/Core/Record/Service.php
Normal file
File diff suppressed because it is too large
Load Diff
84
application/Espo/Core/Record/ServiceContainer.php
Normal file
84
application/Espo/Core/Record/ServiceContainer.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Repository\Util as RepositoryUtil;
|
||||
|
||||
/**
|
||||
* Container for record services. Lazy loading is used.
|
||||
* Usually there's no need to have multiple record service instances of the same entity type.
|
||||
* Use this container instead of serviceFactory to get record services.
|
||||
*
|
||||
* Important. Returns record services for the current user.
|
||||
* Use the service-factory to create services for a specific user.
|
||||
*/
|
||||
class ServiceContainer
|
||||
{
|
||||
/** @var array<string, Service<Entity>> */
|
||||
private $data = [];
|
||||
|
||||
public function __construct(private ServiceFactory $serviceFactory)
|
||||
{}
|
||||
|
||||
/**
|
||||
* Get a record service by an entity class name.
|
||||
*
|
||||
* @template T of Entity
|
||||
* @param class-string<T> $className An entity class name.
|
||||
* @return Service<T>
|
||||
*/
|
||||
public function getByClass(string $className): Service
|
||||
{
|
||||
$entityType = RepositoryUtil::getEntityTypeByClass($className);
|
||||
|
||||
/** @var Service<T> */
|
||||
return $this->get($entityType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a record service by an entity type.
|
||||
*
|
||||
* @return Service<Entity>
|
||||
*/
|
||||
public function get(string $entityType): Service
|
||||
{
|
||||
if (!array_key_exists($entityType, $this->data)) {
|
||||
$this->load($entityType);
|
||||
}
|
||||
|
||||
return $this->data[$entityType];
|
||||
}
|
||||
|
||||
private function load(string $entityType): void
|
||||
{
|
||||
$this->data[$entityType] = $this->serviceFactory->create($entityType);
|
||||
}
|
||||
}
|
||||
168
application/Espo/Core/Record/ServiceFactory.php
Normal file
168
application/Espo/Core/Record/ServiceFactory.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record;
|
||||
|
||||
use Espo\Core\ServiceFactory as Factory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\AclManager;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Repository\Util as RepositoryUtil;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Create a service for a specific user.
|
||||
*/
|
||||
class ServiceFactory
|
||||
{
|
||||
private const RECORD_SERVICE_NAME = 'Record';
|
||||
private const RECORD_TREE_SERVICE_NAME = 'RecordTree';
|
||||
|
||||
/** @var array<string, string> */
|
||||
private $defaultTypeMap = [
|
||||
'CategoryTree' => self::RECORD_TREE_SERVICE_NAME,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private Factory $serviceFactory,
|
||||
private Metadata $metadata,
|
||||
private User $user,
|
||||
private Acl $acl,
|
||||
private AclManager $aclManager
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a record service by an entity class name.
|
||||
*
|
||||
* @template T of Entity
|
||||
* @param class-string<T> $className An entity class name.
|
||||
* @return Service<T>
|
||||
*/
|
||||
public function createByClass(string $className): Service
|
||||
{
|
||||
$entityType = RepositoryUtil::getEntityTypeByClass($className);
|
||||
|
||||
/** @var Service<T> */
|
||||
return $this->create($entityType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a record service for a user by an entity class name.
|
||||
*
|
||||
* @template T of Entity
|
||||
* @param class-string<T> $className An entity class name.
|
||||
* @return Service<T>
|
||||
*/
|
||||
public function createByClassForUser(string $className, User $user): Service
|
||||
{
|
||||
$entityType = RepositoryUtil::getEntityTypeByClass($className);
|
||||
|
||||
/** @var Service<T> */
|
||||
return $this->createForUser($entityType, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a record service by an entity type.
|
||||
*
|
||||
* @return Service<Entity>
|
||||
*/
|
||||
public function create(string $entityType): Service
|
||||
{
|
||||
$obj = $this->createInternal($entityType);
|
||||
|
||||
$obj->setUser($this->user);
|
||||
$obj->setAcl($this->acl);
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a record service for a user.
|
||||
*
|
||||
* @return Service<Entity>
|
||||
*/
|
||||
public function createForUser(string $entityType, User $user): Service
|
||||
{
|
||||
$obj = $this->createInternal($entityType);
|
||||
|
||||
$acl = $this->aclManager->createUserAcl($user);
|
||||
|
||||
$obj->setUser($user);
|
||||
$obj->setAcl($acl);
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Service<Entity>
|
||||
*/
|
||||
private function createInternal(string $entityType): Service
|
||||
{
|
||||
if (!$this->metadata->get(['scopes', $entityType, 'entity'])) {
|
||||
throw new RuntimeException("Can't create record service '{$entityType}', there's no such entity type.");
|
||||
}
|
||||
|
||||
if (!$this->serviceFactory->checkExists($entityType)) {
|
||||
return $this->createDefault($entityType);
|
||||
}
|
||||
|
||||
$service = $this->serviceFactory->createWith($entityType, ['entityType' => $entityType]);
|
||||
|
||||
if (!$service instanceof Service) {
|
||||
return $this->createDefault($entityType);
|
||||
}
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Service<Entity>
|
||||
*/
|
||||
private function createDefault(string $entityType): Service
|
||||
{
|
||||
$default = self::RECORD_SERVICE_NAME;
|
||||
|
||||
$type = $this->metadata->get(['scopes', $entityType, 'type']);
|
||||
|
||||
if ($type) {
|
||||
$default = $this->defaultTypeMap[$type] ?? $default;
|
||||
}
|
||||
|
||||
$obj = $this->serviceFactory->createWith($default, ['entityType' => $entityType]);
|
||||
|
||||
if (!$obj instanceof Service) {
|
||||
throw new RuntimeException("Service class {$default} is not instance of Record.");
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
41
application/Espo/Core/Record/UpdateContext.php
Normal file
41
application/Espo/Core/Record/UpdateContext.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @todo Remove in v10.0. Use UpdateResult.
|
||||
*/
|
||||
class UpdateContext
|
||||
{
|
||||
public function __construct(
|
||||
public bool $linkUpdated = false,
|
||||
) {}
|
||||
}
|
||||
95
application/Espo/Core/Record/UpdateParams.php
Normal file
95
application/Espo/Core/Record/UpdateParams.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record;
|
||||
|
||||
/**
|
||||
* Immutable.
|
||||
*/
|
||||
class UpdateParams
|
||||
{
|
||||
private bool $skipDuplicateCheck = false;
|
||||
private ?int $versionNumber = null;
|
||||
|
||||
private ?UpdateContext $context = null;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function withSkipDuplicateCheck(bool $skipDuplicateCheck = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->skipDuplicateCheck = $skipDuplicateCheck;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withVersionNumber(?int $versionNumber): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->versionNumber = $versionNumber;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function skipDuplicateCheck(): bool
|
||||
{
|
||||
return $this->skipDuplicateCheck;
|
||||
}
|
||||
|
||||
public function getVersionNumber(): ?int
|
||||
{
|
||||
return $this->versionNumber;
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @todo Remove in v10.0.
|
||||
*/
|
||||
public function withContext(?UpdateContext $context): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->context = $context;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @todo Remove in v10.0.
|
||||
*/
|
||||
public function getContext(): ?UpdateContext
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
}
|
||||
57
application/Espo/Core/Record/UpdateParamsFetcher.php
Normal file
57
application/Espo/Core/Record/UpdateParamsFetcher.php
Normal 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\Record;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
class UpdateParamsFetcher
|
||||
{
|
||||
public function __construct() {}
|
||||
|
||||
public function fetch(Request $request): UpdateParams
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$skipDuplicateCheck = $request->hasHeader('X-Skip-Duplicate-Check') ?
|
||||
strtolower($request->getHeader('X-Skip-Duplicate-Check') ?? '') === 'true' :
|
||||
$data->_skipDuplicateCheck ?? // legacy
|
||||
false;
|
||||
|
||||
$versionNumber = $request->getHeader('X-Version-Number');
|
||||
|
||||
if ($versionNumber !== null) {
|
||||
$versionNumber = intval($versionNumber);
|
||||
}
|
||||
|
||||
return UpdateParams::create()
|
||||
->withSkipDuplicateCheck($skipDuplicateCheck)
|
||||
->withVersionNumber($versionNumber);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user