Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04178c5af4 | |||
| 718b9ba591 | |||
| 12f7304b12 | |||
| a967d1b6ab | |||
| f60bf6019e | |||
| 9a1582dd7c | |||
| d1ab599390 | |||
| f6c845e627 | |||
| 501de26774 | |||
| 7be93fb699 | |||
| d3d3e1bb82 | |||
| 72ea83fbca | |||
| 83362fe3c1 | |||
| 1aabf6eea6 | |||
| f23634ee61 | |||
| 089bb01e12 | |||
| 7ba772f2cc | |||
| 3ab26a5235 | |||
| ca27df06d9 | |||
| a7f2faa53b | |||
| 6dbca2f584 | |||
| fe505aea1e | |||
| 159284f5ee | |||
| 9ca71c91bf | |||
| d3f03949a1 | |||
| 7c25f2a8fe | |||
| 8a03ad6240 | |||
| 212d383ac1 | |||
| bf26581339 | |||
| 49f7ac30ca | |||
| 7d195793a3 | |||
| e41a1bc410 | |||
| b4e38002fe | |||
| 49f48340ac | |||
| 1877aeecb0 | |||
| 669b237893 | |||
| c22390ffc7 | |||
| 10dd636646 | |||
| 82c7c697e6 | |||
| 8c6eb518d8 | |||
| e94a7953be | |||
| 8561b1af65 | |||
| 9e2029dc0a | |||
| fc616f4fea | |||
| 1f47c5b80f | |||
| 6cfa26ee10 | |||
| ecd95d16f2 | |||
| 0c1e4bf9d8 | |||
| 61677e34e0 | |||
| ca136225cb | |||
| 5462854f53 | |||
| 6902ae0e04 | |||
| 4a60d17936 | |||
| 0fbfe21258 | |||
| 6fa1ff811b | |||
| a0e709dd86 | |||
| 50c63fa986 | |||
| 409cb3fa19 | |||
| 5766355661 | |||
| 04280e68c6 | |||
| 3375ca3181 | |||
| 04794bb2aa | |||
| f6a6aefd5e | |||
| 7296fcfc77 | |||
| 19d3789c24 | |||
| f298d6ff5a | |||
| 6fbd21822e | |||
| 1c270a9d2e | |||
| c52846f480 | |||
| d2f51066c0 | |||
| 7537fd25fd | |||
| 10ccbe4709 | |||
| 3a161ef0e7 | |||
| a778611edf | |||
| cb291c2e5e | |||
| 2cb70969bf | |||
| 3a2eb429f1 | |||
| f7c2611200 | |||
| beef740114 | |||
| 35d96b36ab | |||
| 74a3e9f6bd | |||
| 38ab9de081 | |||
| 669e78e072 | |||
| 9166277a01 | |||
| dcd3ce66ce | |||
| feac638722 | |||
| 29a5d39768 | |||
| 0368d0569c | |||
| 1ba438fa9f | |||
| 198b9c78fc | |||
| 145e521bd5 | |||
| 69b626b4da | |||
| 482e6cc308 | |||
| d678307d2d | |||
| 74b1c2ba98 | |||
| 1c550da572 | |||
| b48b033be8 | |||
| c392fabc14 | |||
| 1de9046b3d | |||
| b007bef366 | |||
| 33c8e202bc | |||
| bbf77b16bd | |||
| 4da48a15a9 | |||
| ee34bf8cba | |||
| 563843cac8 | |||
| 9463c9ea55 | |||
| 39b461a618 | |||
| 65d2c1de07 | |||
| adccaa7951 | |||
| f9d0f3bcaf | |||
| 3321bf1020 | |||
| d3da6fc8cc | |||
| 6c0e3c67a8 | |||
| 2cd9b532f0 | |||
| ce1e664d63 | |||
| 2f97cf36ea | |||
| d7764af73d | |||
| dc738fdbfe | |||
| 44c20522a0 | |||
| 3161d0c436 | |||
| 6930658c27 | |||
| 61e427d621 | |||
| ecad172891 | |||
| a694db623c | |||
| ed7677a4d7 |
@@ -3,8 +3,9 @@
|
||||
#### Guidelines
|
||||
|
||||
When reporting a possible bug, provide detail steps so that we will be able
|
||||
to reproduce the issue. Steps to reproduce should be clear and unambiguous. Try not to use phrases like "very big bug",
|
||||
"huge issue", "useless feature", etc. No need to use exclamation marks as well.
|
||||
to reproduce the issue. Steps to reproduce should be clear and unambiguous.
|
||||
|
||||
Try not to use phrases like "very big bug", "huge issue", "useless feature", etc. No need to use exclamation marks as well.
|
||||
|
||||
#### No support
|
||||
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
|
||||
If you believe you have discovered a vulnerability in EspoCRM, please contacts us via [this](https://www.espocrm.com/contacts/) or [this](https://www.espocrm.com/support/) forms. Or create a private vulnerability report on GitHub.
|
||||
|
||||
### LLM slop
|
||||
|
||||
Low-effort, LLM-generated reports are not allowed. In such cases, if it is evident that the author does not understand the subject they submitted, the account will be blocked.
|
||||
|
||||
## Supported versions
|
||||
|
||||
For severe vulnerabilities we provide fixes for 2 minor versions (the second number in the version string) back from the current stable version.
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
###############################################################################
|
||||
# EspoCRM - Development Server for Extensions
|
||||
# Based on official image, overlays custom code on each deploy
|
||||
###############################################################################
|
||||
|
||||
FROM espocrm/espocrm:latest
|
||||
|
||||
# Fix redirect loop: tell Apache it's behind HTTPS proxy (Traefik)
|
||||
RUN echo 'SetEnv HTTPS on' > /etc/apache2/conf-available/force-https.conf && \
|
||||
a2enconf force-https
|
||||
|
||||
# Copy custom extensions and modifications
|
||||
COPY custom/ /var/www/html/custom/
|
||||
|
||||
# Ensure correct permissions
|
||||
RUN chown -R www-data:www-data /var/www/html/custom
|
||||
|
||||
EXPOSE 80
|
||||
@@ -242,6 +242,11 @@ class Binding implements BindingProcessor
|
||||
'Espo\\Core\\Utils\\Config\\ApplicationConfig',
|
||||
'applicationConfig'
|
||||
);
|
||||
|
||||
$binder->bindService(
|
||||
'Espo\\Core\\Session\\Session',
|
||||
'session'
|
||||
);
|
||||
}
|
||||
|
||||
private function bindCore(Binder $binder): void
|
||||
|
||||
@@ -280,7 +280,7 @@ class Email implements AssignmentNotificator
|
||||
->setType(Notification::TYPE_EMAIL_RECEIVED)
|
||||
->setUserId($userId)
|
||||
->setData($data)
|
||||
->setRelated(LinkParent::createFromEntity($entity))
|
||||
->setRelated(LinkParent::fromEntity($entity))
|
||||
->setActionId($params->getActionId());
|
||||
|
||||
$this->entityManager->saveEntity($notification);
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldValidators\Common\Link;
|
||||
|
||||
use Espo\Core\Field\Link;
|
||||
use Espo\Core\FieldValidation\Validator;
|
||||
use Espo\Core\FieldValidation\Validator\Data;
|
||||
use Espo\Core\FieldValidation\Validator\Failure;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\DynamicLogic\CascadingFields\ItemsProvider;
|
||||
use Espo\Tools\DynamicLogic\CascadingFields\ValidationHelper;
|
||||
|
||||
/**
|
||||
* @implements Validator<Entity>
|
||||
*/
|
||||
class CascadingSelect implements Validator
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Defs $defs,
|
||||
private ValidationHelper $helper,
|
||||
private ItemsProvider $itemsProvider,
|
||||
) {}
|
||||
|
||||
public function validate(Entity $entity, string $field, Data $data): ?Failure
|
||||
{
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$items = $this->itemsProvider->get($entity->getEntityType(), $field);
|
||||
|
||||
if (!$items) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$linkValue = $entity->getValueObject($field);
|
||||
|
||||
if (!$linkValue instanceof Link) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entityType = $this->defs
|
||||
->getEntity($entity->getEntityType())
|
||||
->tryGetRelation($field)
|
||||
?->tryGetForeignEntityType();
|
||||
|
||||
if (!$entityType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$valueEntity = $this->entityManager->getEntityById($entityType, $linkValue->getId());
|
||||
|
||||
if (!$valueEntity instanceof CoreEntity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
$itemFailure = $this->helper->validateItem($entity, $valueEntity, $item);
|
||||
|
||||
if ($itemFailure) {
|
||||
return $itemFailure;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldValidators\Common\LinkMultiple;
|
||||
|
||||
use Espo\Core\Field\LinkMultiple;
|
||||
use Espo\Core\FieldValidation\Validator;
|
||||
use Espo\Core\FieldValidation\Validator\Data;
|
||||
use Espo\Core\FieldValidation\Validator\Failure;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
use Espo\Tools\DynamicLogic\CascadingFields\ItemsProvider;
|
||||
use Espo\Tools\DynamicLogic\CascadingFields\ValidationHelper;
|
||||
|
||||
/**
|
||||
* @implements Validator<Entity>
|
||||
*/
|
||||
class CascadingSelect implements Validator
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Defs $defs,
|
||||
private ValidationHelper $helper,
|
||||
private ItemsProvider $itemsProvider,
|
||||
) {}
|
||||
|
||||
public function validate(Entity $entity, string $field, Data $data): ?Failure
|
||||
{
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$items = $this->itemsProvider->get($entity->getEntityType(), $field);
|
||||
|
||||
if (!$items) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$linkValue = $entity->getValueObject($field);
|
||||
|
||||
if (!$linkValue instanceof LinkMultiple || !$linkValue->getIdList()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entityType = $this->defs
|
||||
->getEntity($entity->getEntityType())
|
||||
->tryGetRelation($field)
|
||||
?->tryGetForeignEntityType();
|
||||
|
||||
if (!$entityType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$valueEntities = $this->entityManager
|
||||
->getRDBRepository($entityType)
|
||||
->where([
|
||||
Attribute::ID => $linkValue->getIdList(),
|
||||
])
|
||||
->find();
|
||||
|
||||
if (!count($valueEntities)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($valueEntities as $valueEntity) {
|
||||
if (!$valueEntity instanceof CoreEntity) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
$itemFailure = $this->helper->validateItem($entity, $valueEntity, $item);
|
||||
|
||||
if ($itemFailure) {
|
||||
return $itemFailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -30,19 +30,17 @@
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class UrlType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
|
||||
private VarcharType $varcharType;
|
||||
|
||||
public function __construct(Metadata $metadata, VarcharType $varcharType)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->varcharType = $varcharType;
|
||||
}
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private VarcharType $varcharType,
|
||||
private Defs $defs,
|
||||
) {}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
@@ -62,6 +60,15 @@ class UrlType
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->defs
|
||||
->getEntity($entity->getEntityType())
|
||||
->tryGetField($field)
|
||||
?->getParam('protocolRequired')
|
||||
) {
|
||||
return filter_var($value, FILTER_VALIDATE_URL) !== false;
|
||||
}
|
||||
|
||||
/** @var string $pattern */
|
||||
$pattern = $this->metadata->get(['app', 'regExpPatterns', 'uriOptionalProtocol', 'pattern']);
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Record\Common\Lock;
|
||||
|
||||
use Espo\Core\Record\DeleteParams;
|
||||
use Espo\Core\Record\Hook\DeleteHook;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Tools\Lock\LockValidationHelper;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class BeforeDeleteValidation implements DeleteHook
|
||||
{
|
||||
public function __construct(
|
||||
private LockValidationHelper $lockValidationHelper,
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity, DeleteParams $params): void
|
||||
{
|
||||
$this->lockValidationHelper->validateBeforeRemove($entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Record\Common\Lock;
|
||||
|
||||
use Espo\Core\Record\Hook\SaveHook;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Tools\Lock\LockValidationHelper;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class BeforeSaveValidation implements SaveHook
|
||||
{
|
||||
public function __construct(
|
||||
private LockValidationHelper $lockValidationHelper,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function process(Entity $entity): void
|
||||
{
|
||||
$this->lockValidationHelper->validateBeforeSave($entity);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Defs\Params\RelationParam;
|
||||
use Espo\Tools\EntityManager\EntityManager as EntityManagerTool;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
@@ -443,11 +444,15 @@ class EntityManager
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
/** @var array{readOnly?: bool} $params */
|
||||
/** @var array{readOnly?: bool, cascadeRemocal?: bool} $params */
|
||||
$params = [];
|
||||
|
||||
if (property_exists($rawParams, 'readOnly')) {
|
||||
$params['readOnly'] = (bool) $rawParams->readOnly;
|
||||
if (property_exists($rawParams, RelationParam::READ_ONLY)) {
|
||||
$params[RelationParam::READ_ONLY] = (bool) $rawParams->{RelationParam::READ_ONLY};
|
||||
}
|
||||
|
||||
if (property_exists($rawParams, RelationParam::CASCADE_REMOVAL)) {
|
||||
$params[RelationParam::CASCADE_REMOVAL] = (bool) $rawParams->{RelationParam::CASCADE_REMOVAL};
|
||||
}
|
||||
|
||||
$this->linkManager->updateParams($entityType, $link, $params);
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Entities\Notification as NotificationEntity;
|
||||
use Espo\Tools\Notification\RecordService as Service;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
@@ -60,6 +61,7 @@ class Notification extends RecordBase
|
||||
$maxSize = $searchParamsAux->getMaxSize();
|
||||
|
||||
$after = $request->getQueryParam('after');
|
||||
$beforeNumber = $request->getQueryParam('beforeNumber');
|
||||
|
||||
$searchParams = SearchParams
|
||||
::create()
|
||||
@@ -76,19 +78,20 @@ class Notification extends RecordBase
|
||||
->setValue($after)
|
||||
->build()
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
$recordCollection = $this->getNotificationService()->get($this->user, $searchParams);
|
||||
$recordCollection = $this->getNotificationService()->get($this->user, $searchParams, $beforeNumber);
|
||||
|
||||
return $recordCollection->toApiOutput();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function getActionNotReadCount(): int
|
||||
{
|
||||
$userId = $this->user->getId();
|
||||
|
||||
return $this->getNotificationService()->getNotReadCount($userId);
|
||||
return $this->getNotificationService()->getNotReadCount($this->user);
|
||||
}
|
||||
|
||||
public function postActionMarkAllRead(Request $request): bool
|
||||
|
||||
@@ -276,6 +276,21 @@ class Acl
|
||||
return $this->aclManager->checkField($this->user, $scope, $field, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access to a link.
|
||||
*
|
||||
* @param string $scope A scope (entity type).
|
||||
* @param string $link A link to check.
|
||||
* @param Table::ACTION_READ|Table::ACTION_EDIT $action An action.
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
public function checkLink(string $scope, string $link, string $action = Table::ACTION_READ): bool
|
||||
{
|
||||
return $this->aclManager->checkLink($this->user, $scope, $link, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get links forbidden for a user.
|
||||
*
|
||||
|
||||
@@ -38,6 +38,7 @@ use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Defs\Params\FieldParam;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
@@ -209,7 +210,11 @@ class Helper
|
||||
$userIds = $entity->getLinkMultipleIdList($field);
|
||||
|
||||
if ($userIds === []) {
|
||||
if ($assignmentPermission === Table::LEVEL_NO && !$user->isApi()) {
|
||||
if (
|
||||
$assignmentPermission === Table::LEVEL_NO &&
|
||||
!$user->isApi() &&
|
||||
$field !== Field::COLLABORATORS
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -329,6 +334,7 @@ class Helper
|
||||
}
|
||||
|
||||
return $fieldDefs->getType() === FieldType::LINK &&
|
||||
!$fieldDefs->getParam(FieldParam::DISABLED) &&
|
||||
$entityDefs->hasRelation(Field::ASSIGNED_USER) &&
|
||||
$entityDefs->getRelation(Field::ASSIGNED_USER)->getForeignEntityType() === User::ENTITY_TYPE;
|
||||
}
|
||||
|
||||
@@ -187,6 +187,13 @@ class GlobalRestriction
|
||||
$value = $this->metadata->get(['entityDefs', $scope, 'links', $link, $type]);
|
||||
}
|
||||
|
||||
if (
|
||||
$type === self::TYPE_FORBIDDEN &&
|
||||
$this->metadata->get("entityDefs.$scope.links.$link.disabled")
|
||||
) {
|
||||
$value = true;
|
||||
}
|
||||
|
||||
if (!$value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -43,4 +43,5 @@ class Permission
|
||||
public const USER_CALENDAR = 'userCalendar';
|
||||
public const FOLLOWER_MANAGEMENT = 'followerManagement';
|
||||
public const GROUP_EMAIL_ACCOUNT = 'groupEmailAccount';
|
||||
public const LOCK = 'lock';
|
||||
}
|
||||
|
||||
@@ -579,6 +579,21 @@ class AclManager
|
||||
return !in_array($field, $this->getScopeForbiddenFieldList($user, $scope, $action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access to a link.
|
||||
*
|
||||
* @param string $scope A scope (entity type).
|
||||
* @param string $link A link to check.
|
||||
* @param Table::ACTION_READ|Table::ACTION_EDIT $action An action.
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
public function checkLink(User $user, string $scope, string $link, string $action = Table::ACTION_READ): bool
|
||||
{
|
||||
return !in_array($link, $this->getScopeForbiddenLinkList($user, $scope, $action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a user has access to another user over a specific permission.
|
||||
*
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Core\Action;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
@@ -39,6 +40,7 @@ interface Action
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
* @throws NotFound
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function process(Params $params, Data $data): void;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Action\Actions;
|
||||
|
||||
use Espo\Core\Action\Action;
|
||||
use Espo\Core\Action\Data;
|
||||
use Espo\Core\Action\Params;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Record\EntityProvider;
|
||||
use Espo\Tools\Lock\LockService;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class Lock implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private LockService $lockService,
|
||||
private EntityProvider $entityProvider,
|
||||
) {}
|
||||
|
||||
public function process(Params $params, Data $data): void
|
||||
{
|
||||
$entityType = $params->getEntityType();
|
||||
$id = $params->getId();
|
||||
|
||||
if (!$this->lockService->isEnabled($entityType)) {
|
||||
throw new ForbiddenSilent("Not enabled.");
|
||||
}
|
||||
|
||||
if (!$this->lockService->isAllowed($entityType)) {
|
||||
throw new ForbiddenSilent("Not allowed.");
|
||||
}
|
||||
|
||||
$entity = $this->entityProvider->get($entityType, $id);
|
||||
|
||||
try {
|
||||
$this->lockService->lock($entity);
|
||||
} catch (Error $e) {
|
||||
throw new Forbidden(previous: $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,14 +32,17 @@ namespace Espo\Core\Action\Actions\Merge;
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Action\Params;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Name\Link;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Record\ActionHistory\Action;
|
||||
use Espo\Core\Record\DeleteParams;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Record\UpdateParams;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\ObjectUtil;
|
||||
use Espo\Entities\Attachment;
|
||||
@@ -47,7 +50,6 @@ use Espo\Entities\Note;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\EmailAddress;
|
||||
use Espo\Entities\PhoneNumber;
|
||||
|
||||
use Espo\ORM\Type\RelationType;
|
||||
use stdClass;
|
||||
|
||||
@@ -64,8 +66,25 @@ class Merger
|
||||
* @param string[] $sourceIdList
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
* @throws BadRequest
|
||||
* @noinspection PhpDocRedundantThrowsInspection
|
||||
*/
|
||||
public function process(Params $params, array $sourceIdList, stdClass $data): void
|
||||
{
|
||||
$this->entityManager->getTransactionManager()->run(function () use ($params, $sourceIdList, $data) {
|
||||
$this->processInternal($params, $sourceIdList, $data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $sourceIdList
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function processInternal(Params $params, array $sourceIdList, stdClass $data): void
|
||||
{
|
||||
$clonedData = ObjectUtil::clone($data);
|
||||
|
||||
@@ -132,18 +151,10 @@ class Merger
|
||||
$this->updateNotes($sourceEntity, $entity);
|
||||
}
|
||||
|
||||
$mergeLinkList = $this->getMergeLinkList($entityType);
|
||||
$this->mergeLinks($entity, $sourceEntityList);
|
||||
|
||||
foreach ($sourceEntityList as $sourceEntity) {
|
||||
foreach ($mergeLinkList as $link) {
|
||||
$this->updateRelations($sourceEntity, $entity, $link);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($sourceEntityList as $sourceEntity) {
|
||||
$this->entityManager->removeEntity($sourceEntity);
|
||||
|
||||
$service->processActionHistoryRecord(Action::DELETE, $sourceEntity);
|
||||
$service->delete($sourceEntity->getId(), DeleteParams::create());
|
||||
}
|
||||
|
||||
if ($hasPhoneNumber) {
|
||||
@@ -154,11 +165,7 @@ class Merger
|
||||
$this->prepareEmailAddressData($emailAddressToRelateList, $clonedData);
|
||||
}
|
||||
|
||||
$entity->set($clonedData);
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
|
||||
$service->processActionHistoryRecord(Action::UPDATE, $entity);
|
||||
$service->update($id, $clonedData, UpdateParams::create()->withSkipDuplicateCheck());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -436,4 +443,18 @@ class Merger
|
||||
|
||||
return $columnAttributeMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Entity[] $sourceEntityList
|
||||
*/
|
||||
private function mergeLinks(Entity $entity, array $sourceEntityList): void
|
||||
{
|
||||
$mergeLinkList = $this->getMergeLinkList($entity->getEntityType());
|
||||
|
||||
foreach ($sourceEntityList as $sourceEntity) {
|
||||
foreach ($mergeLinkList as $link) {
|
||||
$this->updateRelations($sourceEntity, $entity, $link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Action\Actions;
|
||||
|
||||
use Espo\Core\Action\Action;
|
||||
use Espo\Core\Action\Data;
|
||||
use Espo\Core\Action\Params;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Record\EntityProvider;
|
||||
use Espo\Tools\Lock\LockService;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class Unlock implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private LockService $lockService,
|
||||
private EntityProvider $entityProvider,
|
||||
) {}
|
||||
|
||||
public function process(Params $params, Data $data): void
|
||||
{
|
||||
$entityType = $params->getEntityType();
|
||||
$id = $params->getId();
|
||||
|
||||
if (!$this->lockService->isEnabled($entityType)) {
|
||||
throw new ForbiddenSilent("Not enabled.");
|
||||
}
|
||||
|
||||
if (!$this->lockService->isAllowed($entityType)) {
|
||||
throw new ForbiddenSilent("Not allowed.");
|
||||
}
|
||||
|
||||
$entity = $this->entityProvider->get($entityType, $id);
|
||||
|
||||
try {
|
||||
$this->lockService->unlock($entity);
|
||||
} catch (Error $e) {
|
||||
throw new Forbidden(previous: $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ namespace Espo\Core\Action;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
@@ -55,6 +56,7 @@ class Service
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
* @throws NotFound
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function process(string $entityType, string $action, string $id, stdClass $data): Entity
|
||||
{
|
||||
|
||||
@@ -39,6 +39,7 @@ use Espo\Core\Exceptions\HasLogMessage;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Utils\Log;
|
||||
|
||||
use Espo\ORM\Exceptions\ValidationException;
|
||||
use LogicException;
|
||||
use Psr\Log\LogLevel;
|
||||
use RuntimeException;
|
||||
@@ -82,6 +83,11 @@ class ErrorOutput
|
||||
NotFound::class,
|
||||
];
|
||||
|
||||
/** @var array<class-string<Throwable>, int> */
|
||||
private array $statusCodeMap = [
|
||||
ValidationException::class => 409,
|
||||
];
|
||||
|
||||
public function __construct(private Log $log)
|
||||
{}
|
||||
|
||||
@@ -136,6 +142,8 @@ class ErrorOutput
|
||||
'request' => $request,
|
||||
]);
|
||||
|
||||
$statusCode = $this->statusCodeMap[$exception::class] ?? $statusCode;
|
||||
|
||||
if (!in_array($statusCode, $this->allowedStatusCodeList)) {
|
||||
$statusCode = 500;
|
||||
}
|
||||
|
||||
@@ -222,6 +222,11 @@ class ConfigDataProvider
|
||||
return $this->object->get('oidcAuthorizationPrompt') ?? 'consent';
|
||||
}
|
||||
|
||||
public function useAuthorizationPkce(): bool
|
||||
{
|
||||
return (bool) $this->object->get('oidcAuthorizationPkce');
|
||||
}
|
||||
|
||||
public function getAuthorizationMaxAge(): ?int
|
||||
{
|
||||
return $this->config->get('oidcAuthorizationMaxAge');
|
||||
|
||||
@@ -41,6 +41,7 @@ use Espo\Core\Authentication\Jwt\Validator;
|
||||
use Espo\Core\Authentication\Oidc\UserProvider\UserInfo;
|
||||
use Espo\Core\Authentication\Result;
|
||||
use Espo\Core\Authentication\Result\FailReason;
|
||||
use Espo\Core\Session\Session;
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Core\Utils\Log;
|
||||
use JsonException;
|
||||
@@ -57,6 +58,8 @@ class Login implements LoginInterface
|
||||
private const REQUEST_TIMEOUT = 10;
|
||||
private const NONCE_HEADER = 'X-Oidc-Authorization-Nonce';
|
||||
|
||||
public const string SESSION_KEY_CODE_VERIFIER = 'oidcCodeVerifier';
|
||||
|
||||
public function __construct(
|
||||
private Espo $espoLogin,
|
||||
private Log $log,
|
||||
@@ -66,6 +69,7 @@ class Login implements LoginInterface
|
||||
private UserProvider $userProvider,
|
||||
private ApplicationState $applicationState,
|
||||
private UserInfoDataProvider $userInfoDataProvider,
|
||||
private Session $session,
|
||||
) {}
|
||||
|
||||
public function login(Data $data, Request $request): Result
|
||||
@@ -214,6 +218,7 @@ class Login implements LoginInterface
|
||||
string $redirectUri,
|
||||
string $clientSecret
|
||||
): array {
|
||||
|
||||
$params = [
|
||||
'grant_type' => 'authorization_code',
|
||||
'client_id' => $clientId,
|
||||
@@ -222,6 +227,12 @@ class Login implements LoginInterface
|
||||
'redirect_uri' => $redirectUri,
|
||||
];
|
||||
|
||||
if ($this->configDataProvider->useAuthorizationPkce()) {
|
||||
$codeVerifier = $this->session->get(self::SESSION_KEY_CODE_VERIFIER);
|
||||
|
||||
$params['code_verifier'] = $codeVerifier;
|
||||
}
|
||||
|
||||
$curl = curl_init();
|
||||
|
||||
curl_setopt_array($curl, [
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication\Oidc;
|
||||
|
||||
class PkceUtil
|
||||
{
|
||||
private const string CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
||||
private const int CODE_LENGTH = 64;
|
||||
|
||||
public static function generateCodeVerifier(): string
|
||||
{
|
||||
$output = '';
|
||||
|
||||
for ($i = 0; $i < self::CODE_LENGTH; $i++) {
|
||||
$output .= self::CHARACTERS[random_int(0, strlen(self::CHARACTERS) - 1)];
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public static function hashAndEncodeCodeVerifier(string $codeVerifier): string
|
||||
{
|
||||
$code = hash('sha256', $codeVerifier, true);
|
||||
|
||||
return rtrim(strtr(base64_encode($code), '+/', '-_'), '=');
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
namespace Espo\Core\Field;
|
||||
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\ORM\Entity;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
@@ -39,6 +41,9 @@ class Link
|
||||
private string $id;
|
||||
private ?string $name = null;
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(string $id)
|
||||
{
|
||||
if (!$id) {
|
||||
@@ -78,9 +83,23 @@ class Link
|
||||
|
||||
/**
|
||||
* Create from an ID.
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public static function create(string $id, ?string $name = null): self
|
||||
{
|
||||
return (new self($id))->withName($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from an entity.
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
public static function fromEntity(Entity $entity): self
|
||||
{
|
||||
return self::create($entity->getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
namespace Espo\Core\Field;
|
||||
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\ORM\Entity;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
@@ -99,8 +101,10 @@ class LinkMultipleItem
|
||||
|
||||
/**
|
||||
* Clone with a name.
|
||||
*
|
||||
* @param ?string $name Is nullable since 9.4.0.
|
||||
*/
|
||||
public function withName(string $name): self
|
||||
public function withName(?string $name): self
|
||||
{
|
||||
$obj = $this->clone();
|
||||
$obj->name = $name;
|
||||
@@ -134,6 +138,30 @@ class LinkMultipleItem
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from a link.
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
public static function fromLink(Link $link): self
|
||||
{
|
||||
return self::create($link->getId(), $link->getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from an entity.
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
public static function fromEntity(Entity $entity): self
|
||||
{
|
||||
return self::create($entity->getId());
|
||||
}
|
||||
|
||||
private function clone(): self
|
||||
{
|
||||
$obj = new self($this->id);
|
||||
|
||||
@@ -103,9 +103,22 @@ class LinkParent
|
||||
|
||||
/**
|
||||
* Create from an entity.
|
||||
*
|
||||
* @deprecated Since 9.4.0. Use `fromEntity`.
|
||||
* @todo Remove in v12.0.
|
||||
*/
|
||||
public static function createFromEntity(Entity $entity): self
|
||||
{
|
||||
return new self($entity->getEntityType(), $entity->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from an entity.
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
public static function fromEntity(Entity $entity): self
|
||||
{
|
||||
return new self($entity->getEntityType(), $entity->getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ use Espo\Core\ORM\Entity as CoreEntity;
|
||||
|
||||
use Espo\Core\FieldProcessing\Saver\Params;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Core\ORM\Repository\Option\SaveContext;
|
||||
use Espo\ORM\Repository\Option\SaveContext;
|
||||
use Espo\Core\ORM\Repository\Option\SaveOption;
|
||||
use Espo\ORM\Defs\Params\RelationParam;
|
||||
use RuntimeException;
|
||||
|
||||
@@ -36,6 +36,8 @@ use Espo\Core\Hook\Hook\AfterSave;
|
||||
use Espo\Core\Hook\Hook\AfterUnrelate;
|
||||
use Espo\Core\Hook\Hook\BeforeRemove;
|
||||
use Espo\Core\Hook\Hook\BeforeSave;
|
||||
use Espo\Core\Hook\Hook\LateAfterRemove;
|
||||
use Espo\Core\Hook\Hook\LateAfterSave;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Query\Select;
|
||||
use Espo\ORM\Repository\Option\MassRelateOptions;
|
||||
@@ -52,8 +54,10 @@ class GeneralInvoker
|
||||
{
|
||||
private const HOOK_BEFORE_SAVE = 'beforeSave';
|
||||
private const HOOK_AFTER_SAVE = 'afterSave';
|
||||
private const HOOK_LATE_AFTER_SAVE = 'lateAfterSave';
|
||||
private const HOOK_BEFORE_REMOVE = 'beforeRemove';
|
||||
private const HOOK_AFTER_REMOVE = 'afterRemove';
|
||||
private const HOOK_LATE_AFTER_REMOVE = 'lateAfterRemove';
|
||||
private const HOOK_AFTER_RELATE = 'afterRelate';
|
||||
private const HOOK_AFTER_UNRELATE = 'afterUnrelate';
|
||||
private const HOOK_AFTER_MASS_RELATE = 'afterMassRelate';
|
||||
@@ -93,6 +97,16 @@ class GeneralInvoker
|
||||
return;
|
||||
}
|
||||
|
||||
if ($name === self::HOOK_LATE_AFTER_SAVE && $hook instanceof LateAfterSave) {
|
||||
if (!$subject instanceof Entity) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
$hook->lateAfterSave($subject, SaveOptions::fromAssoc($options));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($name === self::HOOK_BEFORE_REMOVE && $hook instanceof BeforeRemove) {
|
||||
if (!$subject instanceof Entity) {
|
||||
throw new LogicException();
|
||||
@@ -113,6 +127,16 @@ class GeneralInvoker
|
||||
return;
|
||||
}
|
||||
|
||||
if ($name === self::HOOK_LATE_AFTER_REMOVE && $hook instanceof LateAfterRemove) {
|
||||
if (!$subject instanceof Entity) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
$hook->lateAfterRemove($subject, RemoveOptions::fromAssoc($options));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($name === self::HOOK_AFTER_RELATE && $hook instanceof AfterRelate) {
|
||||
$relationName = $hookData['relationName'] ?? null;
|
||||
$relatedEntity = $hookData['foreignEntity'] ?? null;
|
||||
|
||||
@@ -40,7 +40,7 @@ use Espo\ORM\Repository\Option\RemoveOptions;
|
||||
interface AfterRemove
|
||||
{
|
||||
/**
|
||||
* Processed after an entity is remove from within a repository.
|
||||
* Processed after an entity is removed from within a repository.
|
||||
*
|
||||
* @param TEntity $entity An entity.
|
||||
* @param RemoveOptions $options Options.
|
||||
|
||||
@@ -40,7 +40,7 @@ use Espo\ORM\Repository\Option\RemoveOptions;
|
||||
interface BeforeRemove
|
||||
{
|
||||
/**
|
||||
* Processed before an entity is remove from within a repository.
|
||||
* Processed before an entity is removed from within a repository.
|
||||
*
|
||||
* @param TEntity $entity An entity.
|
||||
* @param RemoveOptions $options Options.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Hook\Hook;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Repository\Option\RemoveOptions;
|
||||
|
||||
/**
|
||||
* A lateAfterRemove hook.
|
||||
*
|
||||
* @template TEntity of Entity = Entity
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
interface LateAfterRemove
|
||||
{
|
||||
/**
|
||||
* Processed after an entity is removed, after the transaction (if one is used).
|
||||
*
|
||||
* @param TEntity $entity An entity.
|
||||
* @param RemoveOptions $options Options.
|
||||
*/
|
||||
public function lateAfterRemove(Entity $entity, RemoveOptions $options): void;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Hook\Hook;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Repository\Option\SaveOptions;
|
||||
|
||||
/**
|
||||
* A lateAfterSave hook.
|
||||
*
|
||||
* @template TEntity of Entity = Entity
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
interface LateAfterSave
|
||||
{
|
||||
/**
|
||||
* Processed after an entity is saved, after the transaction (if one is used).
|
||||
* To check whether the entity was new before save, obtain the save context
|
||||
* from the options and call `isNew`.
|
||||
*
|
||||
* @param TEntity $entity An entity.
|
||||
* @param SaveOptions $options Options.
|
||||
*/
|
||||
public function lateAfterSave(Entity $entity, SaveOptions $options): void;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\HttpClient;
|
||||
|
||||
use Espo\Core\HttpClient\Exceptions\ConnectException;
|
||||
use Espo\Core\HttpClient\Exceptions\NotAllowedInternalHost;
|
||||
use Espo\Core\HttpClient\Exceptions\TooManyRedirectsException;
|
||||
use Espo\Core\Utils\Security\UrlCheck;
|
||||
use GuzzleHttp;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use RuntimeException;
|
||||
|
||||
use const CURLE_OPERATION_TIMEDOUT;
|
||||
|
||||
class Client
|
||||
{
|
||||
private const int MAX_REDIRECT_NUMBER = 5;
|
||||
|
||||
/**
|
||||
* To be instantiated with the ClientFactory.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function __construct(
|
||||
private Options $options,
|
||||
private UrlCheck $urlCheck,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Send a request. Does not throw exceptions on error responses.
|
||||
*
|
||||
* @throws TooManyRedirectsException
|
||||
* @throws ConnectException
|
||||
*/
|
||||
public function send(RequestInterface $request): ResponseInterface
|
||||
{
|
||||
$options = [
|
||||
'protocols' => array_map(
|
||||
fn (Protocol $protocol) => $protocol->value,
|
||||
$this->options->redirect->protocols
|
||||
),
|
||||
'allow_redirects' => false,
|
||||
'http_errors' => false,
|
||||
];
|
||||
|
||||
if ($this->options->redirect->allow) {
|
||||
$options['allow_redirects'] = [
|
||||
'max' => $this->options->redirect->maxNumber ?? self::MAX_REDIRECT_NUMBER,
|
||||
'strict' => $this->options->redirect->strict,
|
||||
'protocols' => array_map(
|
||||
fn (Protocol $protocol) => $protocol->value,
|
||||
$this->options->redirect->protocols
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->options->timeout !== null) {
|
||||
$options['timeout'] = $this->options->timeout;
|
||||
}
|
||||
|
||||
if ($this->options->connectTimeout !== null) {
|
||||
$options['connect_timeout'] = $this->options->connectTimeout;
|
||||
}
|
||||
|
||||
if ($this->options->internalHostRestriction->restrict) {
|
||||
$stack = GuzzleHttp\HandlerStack::create();
|
||||
|
||||
$stack->push(
|
||||
GuzzleHttp\Middleware::mapRequest(function (RequestInterface $request) {
|
||||
$url = (string) $request->getUri();
|
||||
|
||||
$this->checkUrl($url, $this->options->internalHostRestriction->allowed);
|
||||
|
||||
return $request;
|
||||
})
|
||||
);
|
||||
|
||||
$options['handler'] = $stack;
|
||||
}
|
||||
|
||||
$client = new GuzzleHttp\Client($options);
|
||||
|
||||
try {
|
||||
return $client->send($request);
|
||||
} catch (GuzzleHttp\Exception\ConnectException $e) {
|
||||
$context = $e->getHandlerContext();
|
||||
|
||||
$reason = null;
|
||||
|
||||
if (($context['errno'] ?? 0) === CURLE_OPERATION_TIMEDOUT) {
|
||||
$reason = ConnectErrorReason::Timeout;
|
||||
}
|
||||
|
||||
throw ConnectException::create(previous: $e, reason: $reason);
|
||||
} catch (GuzzleHttp\Exception\TooManyRedirectsException $e) {
|
||||
throw new TooManyRedirectsException(previous: $e);
|
||||
} catch (GuzzleHttp\Exception\GuzzleException $e) {
|
||||
throw new RuntimeException(previous: $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $allowed
|
||||
* @throws NotAllowedInternalHost
|
||||
*/
|
||||
private function checkUrl(string $url, array $allowed): void
|
||||
{
|
||||
if (
|
||||
!Util::matchUrlToAddressList($url, $allowed) &&
|
||||
!$this->urlCheck->isNotInternalUrl($url)
|
||||
) {
|
||||
throw new NotAllowedInternalHost("Not allowed internal host in '$url'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\HttpClient;
|
||||
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\InjectableFactory;
|
||||
|
||||
/**
|
||||
* An HTTP client factory.
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
class ClientFactory
|
||||
{
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
) {}
|
||||
|
||||
public function create(Options $options): Client
|
||||
{
|
||||
$binding = BindingContainerBuilder::create()
|
||||
->bindInstance(Options::class, $options)
|
||||
->build();
|
||||
|
||||
return $this->injectableFactory->createWithBinding(Client::class, $binding);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\HttpClient;
|
||||
|
||||
enum ConnectErrorReason
|
||||
{
|
||||
case Timeout;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\HttpClient\Exceptions;
|
||||
|
||||
use Espo\Core\HttpClient\ConnectErrorReason;
|
||||
use Exception;
|
||||
|
||||
final class ConnectException extends SendException
|
||||
{
|
||||
private ?ConnectErrorReason $reason = null;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function create(?Exception $previous, ?ConnectErrorReason $reason): ConnectException
|
||||
{
|
||||
$exception = new ConnectException(previous: $previous);
|
||||
$exception->reason = $reason;
|
||||
|
||||
return $exception;
|
||||
}
|
||||
|
||||
public function getReason(): ?ConnectErrorReason
|
||||
{
|
||||
return $this->reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\HttpClient\Exceptions;
|
||||
|
||||
class NotAllowedInternalHost extends SendException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\HttpClient\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
abstract class SendException extends Exception
|
||||
{}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\HttpClient\Exceptions;
|
||||
|
||||
class TooManyRedirectsException extends SendException
|
||||
{}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\HttpClient;
|
||||
|
||||
use Espo\Core\HttpClient\Options\InternalHostRestriction;
|
||||
use Espo\Core\HttpClient\Options\Redirect;
|
||||
|
||||
readonly class Options
|
||||
{
|
||||
/**
|
||||
* @todo SSL options.
|
||||
* Use named parameters when calling.
|
||||
*
|
||||
* @param Protocol[] $protocols
|
||||
*/
|
||||
public function __construct(
|
||||
public array $protocols = [Protocol::https, Protocol::http],
|
||||
public Redirect $redirect = new Redirect(),
|
||||
public ?int $timeout = null,
|
||||
public ?int $connectTimeout = null,
|
||||
public InternalHostRestriction $internalHostRestriction = new InternalHostRestriction(),
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\HttpClient\Options;
|
||||
|
||||
class InternalHostRestriction
|
||||
{
|
||||
/**
|
||||
* @param string[] $allowed A list of `host:port`.
|
||||
*/
|
||||
public function __construct(
|
||||
public bool $restrict = false,
|
||||
public array $allowed = [],
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\HttpClient\Options;
|
||||
|
||||
use Espo\Core\HttpClient\Protocol;
|
||||
|
||||
readonly class Redirect
|
||||
{
|
||||
/**
|
||||
* Use named parameters when calling.
|
||||
*
|
||||
* @param Protocol[] $protocols
|
||||
*/
|
||||
public function __construct(
|
||||
public bool $allow = false,
|
||||
public array $protocols = [Protocol::https],
|
||||
public ?int $maxNumber = null,
|
||||
public bool $strict = false,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\HttpClient;
|
||||
|
||||
enum Protocol: string
|
||||
{
|
||||
case http = 'http';
|
||||
case https = 'https';
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\HttpClient;
|
||||
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
class Util
|
||||
{
|
||||
/**
|
||||
* @param resource|string|int|float|bool|StreamInterface $resource
|
||||
* @since 9.4.0
|
||||
*/
|
||||
public static function streamFor($resource): StreamInterface
|
||||
{
|
||||
return Utils::streamFor($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param string[] $addressList
|
||||
*/
|
||||
public static function matchUrlToAddressList(string $url, array $addressList): bool
|
||||
{
|
||||
if (!$addressList) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
$port = parse_url($url, PHP_URL_PORT);
|
||||
$scheme = parse_url($url, PHP_URL_SCHEME);
|
||||
|
||||
if (!is_string($host)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_int($port)) {
|
||||
if ($scheme === 'https') {
|
||||
$port = 443;
|
||||
} else if ($scheme === 'http') {
|
||||
$port = 80;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_int($port)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$address = $host . ':' . $port;
|
||||
|
||||
return in_array($address, $addressList);
|
||||
}
|
||||
}
|
||||
@@ -613,7 +613,7 @@ class DefaultImporter implements Importer
|
||||
{
|
||||
foreach ($inlineAttachmentList as $attachment) {
|
||||
$attachment->setTargetField('body');
|
||||
$attachment->setRelated(LinkParent::createFromEntity($email));
|
||||
$attachment->setRelated(LinkParent::fromEntity($email));
|
||||
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
}
|
||||
|
||||
@@ -58,6 +58,27 @@ class MailMimeParser implements Parser
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
'pdf' => 'application/pdf',
|
||||
'doc' => 'application/msword',
|
||||
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'xls' => 'application/vnd.ms-excel',
|
||||
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'ppt' => 'application/vnd.ms-powerpoint',
|
||||
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'odt' => 'application/vnd.oasis.opendocument.text',
|
||||
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
|
||||
'csv' => 'text/csv',
|
||||
'txt' => 'text/plain',
|
||||
'html' => 'text/html',
|
||||
'htm' => 'text/html',
|
||||
'xml' => 'application/xml',
|
||||
'zip' => 'application/zip',
|
||||
'gz' => 'application/gzip',
|
||||
'eml' => 'message/rfc822',
|
||||
'svg' => 'image/svg+xml',
|
||||
'bmp' => 'image/bmp',
|
||||
'tif' => 'image/tiff',
|
||||
'tiff' => 'image/tiff',
|
||||
];
|
||||
|
||||
private ?WrappeeParser $parser = null;
|
||||
@@ -434,9 +455,9 @@ class MailMimeParser implements Parser
|
||||
return null;
|
||||
}
|
||||
|
||||
$ext = explode('.', $filename)[1] ?? null;
|
||||
$ext = pathinfo($filename, PATHINFO_EXTENSION);
|
||||
|
||||
if (!$ext) {
|
||||
if ($ext === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\MassAction\Actions;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\ErrorSilent;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\MassAction\Data;
|
||||
use Espo\Core\MassAction\MassAction;
|
||||
use Espo\Core\MassAction\Params;
|
||||
use Espo\Core\MassAction\QueryBuilder;
|
||||
use Espo\Core\MassAction\Result;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\Lock\LockService;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class MassLock implements MassAction
|
||||
{
|
||||
public function __construct(
|
||||
private QueryBuilder $queryBuilder,
|
||||
private LockService $lockService,
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl,
|
||||
) {}
|
||||
|
||||
public function process(Params $params, Data $data): Result
|
||||
{
|
||||
$entityType = $params->getEntityType();
|
||||
|
||||
if (!$this->lockService->isEnabled($entityType)) {
|
||||
throw new ErrorSilent("Not enabled.");
|
||||
}
|
||||
|
||||
if (!$this->lockService->isAllowed($entityType)) {
|
||||
throw new ForbiddenSilent("Not allowed.");
|
||||
}
|
||||
|
||||
if ($this->acl->getPermissionLevel(Acl\Permission::MASS_UPDATE) !== Acl\Table::LEVEL_YES) {
|
||||
throw new ForbiddenSilent("No mass update permission.");
|
||||
}
|
||||
|
||||
$query = $this->queryBuilder->build($params);
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository($entityType)
|
||||
->clone($query)
|
||||
->sth()
|
||||
->find();
|
||||
|
||||
return $this->lockService->massLock($collection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\MassAction\Actions;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\ErrorSilent;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\MassAction\Data;
|
||||
use Espo\Core\MassAction\MassAction;
|
||||
use Espo\Core\MassAction\Params;
|
||||
use Espo\Core\MassAction\QueryBuilder;
|
||||
use Espo\Core\MassAction\Result;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\Lock\LockService;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class MassUnlock implements MassAction
|
||||
{
|
||||
public function __construct(
|
||||
private QueryBuilder $queryBuilder,
|
||||
private LockService $lockService,
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl,
|
||||
) {}
|
||||
|
||||
public function process(Params $params, Data $data): Result
|
||||
{
|
||||
$entityType = $params->getEntityType();
|
||||
|
||||
if (!$this->lockService->isEnabled($entityType)) {
|
||||
throw new ErrorSilent("Not enabled.");
|
||||
}
|
||||
|
||||
if (!$this->lockService->isAllowed($entityType)) {
|
||||
throw new ForbiddenSilent("Not allowed.");
|
||||
}
|
||||
|
||||
if ($this->acl->getPermissionLevel(Acl\Permission::MASS_UPDATE) !== Acl\Table::LEVEL_YES) {
|
||||
throw new ForbiddenSilent("No mass update permission.");
|
||||
}
|
||||
|
||||
$query = $this->queryBuilder->build($params);
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository($entityType)
|
||||
->clone($query)
|
||||
->sth()
|
||||
->find();
|
||||
|
||||
return $this->lockService->massUnlock($collection);
|
||||
}
|
||||
}
|
||||
@@ -49,4 +49,5 @@ class Field
|
||||
public const EMAIL_ADDRESS = 'emailAddress';
|
||||
public const PHONE_NUMBER = 'phoneNumber';
|
||||
public const VERSION_NUMBER = 'versionNumber';
|
||||
public const string IS_LOCKED = 'isLocked';
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ class DefaultAssignmentNotificator implements AssignmentNotificator
|
||||
'userId' => $this->user->getId(),
|
||||
'userName' => $this->user->getName(),
|
||||
])
|
||||
->setRelated(LinkParent::createFromEntity($entity));
|
||||
->setRelated(LinkParent::fromEntity($entity));
|
||||
|
||||
$this->entityManager->saveEntity($notification);
|
||||
}
|
||||
|
||||
@@ -291,6 +291,10 @@ class LinkCheck
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
if (!$this->acl->checkLink($entityType, $link)) {
|
||||
throw new ForbiddenSilent("Link $link is forbidden.");
|
||||
}
|
||||
|
||||
/** @var AclTable::ACTION_*|null $action */
|
||||
$action = $this->getParam($entityType, $link, 'linkRequiredAccess');
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ class DefaultActionLogger implements ActionLogger
|
||||
->setAuthTokenId($this->user->get('authTokenId'))
|
||||
->setAuthLogRecordId($this->user->get('authLogRecordId'))
|
||||
->setIpAddress($this->user->get('ipAddress'))
|
||||
->setTarget(LinkParent::createFromEntity($entity));
|
||||
->setTarget(LinkParent::fromEntity($entity));
|
||||
|
||||
$this->entityManager->saveEntity($historyRecord);
|
||||
}
|
||||
|
||||
@@ -29,12 +29,18 @@
|
||||
|
||||
namespace Espo\Core\Record\Deleted;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Field\DateTime;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\ORM\Repository\Option\SaveOption;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Defs\Params\RelationParam;
|
||||
use Espo\ORM\Defs\RelationDefs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
use Espo\ORM\Repository\Util;
|
||||
|
||||
/**
|
||||
* @implements Restorer<Entity>
|
||||
@@ -44,12 +50,13 @@ class DefaultRestorer implements Restorer
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Metadata $metadata,
|
||||
private RestorerFactory $restorerFactory,
|
||||
) {}
|
||||
|
||||
public function restore(Entity $entity): void
|
||||
{
|
||||
if (!$entity->get(Attribute::DELETED)) {
|
||||
throw new Forbidden("No 'deleted' attribute.");
|
||||
throw new Conflict("Entity is not soft-deleted.");
|
||||
}
|
||||
|
||||
$this->entityManager
|
||||
@@ -57,8 +64,13 @@ class DefaultRestorer implements Restorer
|
||||
->run(fn () => $this->restoreInTransaction($entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Conflict
|
||||
*/
|
||||
private function restoreInTransaction(Entity $entity): void
|
||||
{
|
||||
$modifiedAt = $this->getModifiedAt($entity);
|
||||
|
||||
$repository = $this->entityManager->getRDBRepository($entity->getEntityType());
|
||||
|
||||
$repository->restoreDeleted($entity->getId());
|
||||
@@ -72,5 +84,100 @@ class DefaultRestorer implements Restorer
|
||||
$entity->set('deleteId', '0');
|
||||
$repository->save($entity, [SaveOption::SILENT => true]);
|
||||
}
|
||||
|
||||
if ($modifiedAt) {
|
||||
$this->restoreRelatedRecords($entity, $modifiedAt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Conflict
|
||||
*/
|
||||
private function restoreRelatedRecords(Entity $entity, DateTime $modifiedAt): void
|
||||
{
|
||||
$relations = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entity->getEntityType())
|
||||
->getRelationList();
|
||||
|
||||
foreach ($relations as $relation) {
|
||||
if (!$relation->getParam(RelationParam::CASCADE_REMOVAL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->restoreRelatedLink($entity, $relation, $modifiedAt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Conflict
|
||||
*/
|
||||
private function restoreRelatedLink(Entity $entity, RelationDefs $relation, DateTime $modifiedAt): void
|
||||
{
|
||||
$foreignEntityType = $relation->tryGetForeignEntityType();
|
||||
$foreign = $relation->tryGetForeignRelationName();
|
||||
|
||||
if (!$foreignEntityType || !$foreign) {
|
||||
return;
|
||||
}
|
||||
|
||||
$foreignType = $this->entityManager
|
||||
->getDefs()
|
||||
->tryGetEntity($foreignEntityType)
|
||||
?->tryGetRelation($foreign)
|
||||
?->getType();
|
||||
|
||||
if (!$foreignType) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Util::isRelationshipEligibleForCascadeRemoval($relation->getType(), $foreignType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$link = $relation->getName();
|
||||
|
||||
$builder = SelectBuilder::create()
|
||||
->from($foreignEntityType)
|
||||
->withDeleted();
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRelation($entity, $link)
|
||||
->clone($builder->build())
|
||||
->sth()
|
||||
->where([
|
||||
Attribute::DELETED => true,
|
||||
Field::MODIFIED_AT . '>=' => $modifiedAt->toString(),
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($collection as $relatedEntity) {
|
||||
$this->restoreRelated($relatedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Conflict
|
||||
*/
|
||||
private function restoreRelated(Entity $relatedEntity): void
|
||||
{
|
||||
if (
|
||||
!$relatedEntity->hasAttribute(Attribute::DELETED) ||
|
||||
!$relatedEntity->get(Attribute::DELETED)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$restorer = $this->restorerFactory->create($relatedEntity->getEntityType());
|
||||
|
||||
$restorer->restore($relatedEntity);
|
||||
}
|
||||
|
||||
|
||||
private function getModifiedAt(Entity $entity): ?DateTime
|
||||
{
|
||||
$modifiedAtString = $entity->get(Field::MODIFIED_AT);
|
||||
|
||||
return $modifiedAtString ? DateTime::fromString($modifiedAtString) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,17 +30,15 @@
|
||||
namespace Espo\Core\Record\Deleted;
|
||||
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
* @template TEntity of Entity = Entity
|
||||
*/
|
||||
interface Restorer
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity A deleted entity.
|
||||
* @throws Forbidden
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function restore(Entity $entity): void;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Record\Deleted;
|
||||
|
||||
use Espo\Core\Binding\BindingContainer;
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class RestorerFactory
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private User $user,
|
||||
) {}
|
||||
|
||||
public function create(string $entityType): Restorer
|
||||
{
|
||||
/** @var class-string<Restorer<Entity>> $restorerClassName */
|
||||
$restorerClassName = $this->metadata->get("recordDefs.$entityType.deletedRestorerClassName") ??
|
||||
DefaultRestorer::class;
|
||||
|
||||
/** @var Restorer */
|
||||
return $this->injectableFactory->createWithBinding($restorerClassName, $this->createBinding());
|
||||
}
|
||||
|
||||
private function createBinding(): BindingContainer
|
||||
{
|
||||
return BindingContainerBuilder::create()
|
||||
->bindInstance(User::class, $this->user)
|
||||
->build();
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,15 @@ class EntityDuplicator
|
||||
{
|
||||
$type = $fieldDefs->getType();
|
||||
|
||||
if (in_array($type, [FieldType::AUTOINCREMENT, FieldType::NUMBER])) {
|
||||
if (
|
||||
in_array($type, [
|
||||
FieldType::AUTOINCREMENT,
|
||||
FieldType::NUMBER,
|
||||
FieldType::LINK_ONE,
|
||||
// Additional measure. It's not fetched.
|
||||
FieldType::PASSWORD,
|
||||
])
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ use Espo\ORM\Entity;
|
||||
use Espo\Core\Record\CreateParams;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
* @template TEntity of Entity = Entity
|
||||
*/
|
||||
interface CreateHook
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@ use Espo\ORM\Entity;
|
||||
use Espo\Core\Record\DeleteParams;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
* @template TEntity of Entity = Entity
|
||||
*/
|
||||
interface DeleteHook
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Espo\Core\Record\Hook;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
* @template TEntity of Entity = Entity
|
||||
*/
|
||||
interface LinkHook
|
||||
{
|
||||
|
||||
@@ -97,7 +97,10 @@ class Provider
|
||||
$key = $type . 'HookClassNameList';
|
||||
|
||||
/** @var class-string[] $classNameList */
|
||||
$classNameList = $this->metadata->get(['recordDefs', $entityType, $key]) ?? [];
|
||||
$classNameList = [
|
||||
...$this->metadata->get("app.record.$key", []),
|
||||
...$this->metadata->get("recordDefs.$entityType.$key", [])
|
||||
];
|
||||
|
||||
$interfaces = $this->typeInterfaceListMap[$type] ?? null;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ use Espo\ORM\Entity;
|
||||
use Espo\Core\Record\ReadParams;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
* @template TEntity of Entity = Entity
|
||||
*/
|
||||
interface ReadHook
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ use Espo\ORM\Entity;
|
||||
/**
|
||||
* On record create or update.
|
||||
*
|
||||
* @template TEntity of Entity
|
||||
* @template TEntity of Entity = Entity
|
||||
* @since 8.1.0.
|
||||
*/
|
||||
interface SaveHook
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Espo\Core\Record\Hook;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
* @template TEntity of Entity = Entity
|
||||
*/
|
||||
interface UnlinkHook
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ use Espo\ORM\Entity;
|
||||
use Espo\Core\Record\UpdateParams;
|
||||
|
||||
/**
|
||||
* @template TEntity of Entity
|
||||
* @template TEntity of Entity = Entity
|
||||
*/
|
||||
interface UpdateHook
|
||||
{
|
||||
|
||||
@@ -95,7 +95,10 @@ class FilterProvider
|
||||
private function getCreateClassNameList(string $entityType): array
|
||||
{
|
||||
/** @var class-string<Filter>[] */
|
||||
return $this->metadata->get("recordDefs.$entityType.createInputFilterClassNameList") ?? [];
|
||||
return [
|
||||
...$this->metadata->get("app.record.createInputFilterClassNameList", []),
|
||||
...$this->metadata->get("recordDefs.$entityType.createInputFilterClassNameList", [])
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,6 +107,9 @@ class FilterProvider
|
||||
private function getUpdateClassNameList(string $entityType): array
|
||||
{
|
||||
/** @var class-string<Filter>[] */
|
||||
return $this->metadata->get("recordDefs.$entityType.updateInputFilterClassNameList") ?? [];
|
||||
return [
|
||||
...$this->metadata->get("app.record.updateInputFilterClassNameList", []),
|
||||
...$this->metadata->get("recordDefs.$entityType.updateInputFilterClassNameList", [])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,9 @@ class FilterProvider
|
||||
private function getClassNameList(string $entityType): array
|
||||
{
|
||||
/** @var class-string<Filter<Entity>>[] */
|
||||
return $this->metadata->get("recordDefs.$entityType.outputFilterClassNameList") ?? [];
|
||||
return [
|
||||
...$this->metadata->get("app.record.outputFilterClassNameList", []),
|
||||
...$this->metadata->get("recordDefs.$entityType.outputFilterClassNameList", []),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@ use Espo\Core\FieldSanitize\SanitizeManager;
|
||||
use Espo\Core\ORM\Defs\AttributeParam;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\ORM\Repository\Option\RemoveOption;
|
||||
use Espo\Core\ORM\Repository\Option\SaveContext;
|
||||
use Espo\Core\Record\Deleted\RestorerFactory;
|
||||
use Espo\ORM\Repository\Option\SaveContext;
|
||||
use Espo\Core\ORM\Repository\Option\SaveOption;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Record\Access\LinkCheck;
|
||||
@@ -51,8 +52,6 @@ use Espo\Core\Record\ActionHistory\ActionLogger;
|
||||
use Espo\Core\Record\ConcurrencyControl\OptimisticProcessor;
|
||||
use Espo\Core\Record\Defaults\Populator as DefaultsPopulator;
|
||||
use Espo\Core\Record\Defaults\PopulatorFactory as DefaultsPopulatorFactory;
|
||||
use Espo\Core\Record\Deleted\DefaultRestorer;
|
||||
use Espo\Core\Record\Deleted\Restorer;
|
||||
use Espo\Core\Record\DynamicLogic\InputFilterProcessor;
|
||||
use Espo\Core\Record\Formula\Processor as FormulaProcessor;
|
||||
use Espo\Core\Record\Input\Data;
|
||||
@@ -580,7 +579,7 @@ class Service implements Crud,
|
||||
->getFieldList();
|
||||
|
||||
foreach ($fieldDefsList as $fieldDefs) {
|
||||
if (!$fieldDefs->getParam('readOnlyAfterCreate')) {
|
||||
if (!$fieldDefs->getParam(FieldParam::READ_ONLY_AFTER_CREATE)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -976,12 +975,16 @@ class Service implements Crud,
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
/** @var class-string<Restorer<Entity>> $restorerClassName */
|
||||
$restorerClassName = $this->metadata->get("recordDefs.$this->entityType.deletedRestorerClassName") ??
|
||||
DefaultRestorer::class;
|
||||
if (!$entity->hasAttribute(Attribute::DELETED)) {
|
||||
throw new Forbidden("Record type is not restorable.");
|
||||
}
|
||||
|
||||
/** @var Restorer<Entity> $restorer */
|
||||
$restorer = $this->injectableFactory->createWithBinding($restorerClassName, $this->createBinding());
|
||||
if ($entity->get(Attribute::DELETED) !== true) {
|
||||
throw new Conflict("Record type is not soft-deleted.");
|
||||
}
|
||||
|
||||
$factory = $this->injectableFactory->createWithBinding(RestorerFactory::class, $this->createBinding());
|
||||
$restorer = $factory->create($this->entityType);
|
||||
|
||||
$restorer->restore($entity);
|
||||
}
|
||||
|
||||
@@ -40,8 +40,57 @@ use Espo\ORM\Query\Part\Order;
|
||||
*/
|
||||
class CategoryTree extends Database
|
||||
{
|
||||
private const ATTR_ORDER = 'order';
|
||||
private const ATTR_PARENT_ID = 'parentId';
|
||||
private const string ATTR_ORDER = 'order';
|
||||
private const string ATTR_PARENT_ID = 'parentId';
|
||||
|
||||
private const string PATH_ATTR_ASCENDOR_ID = 'ascendorId';
|
||||
private const string PATH_ATTR_DESCENDOR_ID = 'descendorId';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function save(Entity $entity, array $options = []): void
|
||||
{
|
||||
$this->entityManager
|
||||
->getTransactionManager()
|
||||
->run(function () use ($entity, $options) {
|
||||
if (!$entity->isNew() && $entity->isAttributeChanged(self::ATTR_PARENT_ID)) {
|
||||
$this->lockPathEntries($entity);
|
||||
}
|
||||
|
||||
parent::save($entity, $options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function remove(Entity $entity, array $options = []): void
|
||||
{
|
||||
$this->entityManager
|
||||
->getTransactionManager()
|
||||
->run(function () use ($entity, $options) {
|
||||
$this->lockPathEntries($entity);
|
||||
|
||||
parent::remove($entity, $options);
|
||||
});
|
||||
}
|
||||
|
||||
private function lockPathEntries(Entity $entity): void
|
||||
{
|
||||
$this->entityManager
|
||||
->getRDBRepository($this->getPathEntityType())
|
||||
->sth()
|
||||
->select(Attribute::ID)
|
||||
->where([
|
||||
'OR' => [
|
||||
self::PATH_ATTR_ASCENDOR_ID => $entity->getId(),
|
||||
self::PATH_ATTR_DESCENDOR_ID => $entity->getId(),
|
||||
]
|
||||
])
|
||||
->forUpdate()
|
||||
->find();
|
||||
}
|
||||
|
||||
protected function beforeSave(Entity $entity, array $options = [])
|
||||
{
|
||||
@@ -64,7 +113,7 @@ class CategoryTree extends Database
|
||||
|
||||
$em = $this->entityManager;
|
||||
|
||||
$pathEntityType = $entity->getEntityType() . 'Path';
|
||||
$pathEntityType = $this->getPathEntityType();
|
||||
|
||||
if ($entity->isNew()) {
|
||||
if ($parentId) {
|
||||
@@ -173,7 +222,7 @@ class CategoryTree extends Database
|
||||
{
|
||||
parent::afterRemove($entity, $options);
|
||||
|
||||
$pathEntityType = $entity->getEntityType() . 'Path';
|
||||
$pathEntityType = $this->getPathEntityType();
|
||||
|
||||
$em = $this->entityManager;
|
||||
|
||||
@@ -215,4 +264,9 @@ class CategoryTree extends Database
|
||||
|
||||
$entity->set(self::ATTR_ORDER, $order);
|
||||
}
|
||||
|
||||
private function getPathEntityType(): string
|
||||
{
|
||||
return $this->entityType . 'Path';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,8 @@ class Database extends RDBRepository
|
||||
$this->recordIdGenerator = $recordIdGenerator;
|
||||
|
||||
$this->hooksDisabled = $this->hooksDisabled || $metadata->get("entityDefs.$entityType.hooksDisabled");
|
||||
$this->transactionalSave = $this->transactionalSave ||
|
||||
$metadata->get("entityDefs.$entityType.transactionalSave");
|
||||
|
||||
$hookMediator = null;
|
||||
|
||||
@@ -126,6 +128,17 @@ class Database extends RDBRepository
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function save(Entity $entity, array $options = []): void
|
||||
{
|
||||
$this->prepareSaveInternal($entity, $options);
|
||||
|
||||
parent::save($entity, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
private function prepareSaveInternal(Entity $entity, array $options = []): void
|
||||
{
|
||||
if (
|
||||
$entity->isNew() &&
|
||||
@@ -140,8 +153,28 @@ class Database extends RDBRepository
|
||||
}
|
||||
|
||||
$this->restoreData = [];
|
||||
}
|
||||
|
||||
parent::save($entity, $options);
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
final protected function lateAfterSave(Entity $entity, array $options): void
|
||||
{
|
||||
if (!$this->hooksDisabled && empty($options[SaveOption::SKIP_HOOKS])) {
|
||||
$this->hookManager->process($this->entityType, 'lateAfterSave', $entity, $options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
final protected function lateAfterRemove(Entity $entity, array $options): void
|
||||
{
|
||||
if (!$this->hooksDisabled && empty($options[SaveOption::SKIP_HOOKS])) {
|
||||
$this->hookManager->process($this->entityType, 'lateAfterRemove', $entity, $options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Session;
|
||||
|
||||
use const PHP_SESSION_NONE;
|
||||
|
||||
/**
|
||||
* Do not use directly. Require the Session interface instead.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class DefaultSession implements Session
|
||||
{
|
||||
public function __construct(
|
||||
?string $cacheLimiter = null,
|
||||
?int $cacheExpire = null,
|
||||
) {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
if ($cacheLimiter !== null) {
|
||||
session_cache_limiter($cacheLimiter);
|
||||
}
|
||||
|
||||
if ($cacheExpire !== null) {
|
||||
session_cache_expire($cacheExpire);
|
||||
}
|
||||
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $key): mixed
|
||||
{
|
||||
return $_SESSION[$key] ?? null;
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value): Session
|
||||
{
|
||||
$_SESSION[$key] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function clear(string $key): void
|
||||
{
|
||||
unset($_SESSION[$key]);
|
||||
}
|
||||
|
||||
public function clearAll(): void
|
||||
{
|
||||
session_unset();
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return array_key_exists($key, $_SESSION);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Session;
|
||||
|
||||
/**
|
||||
* A session wrapper.
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
interface Session
|
||||
{
|
||||
public function get(string $key): mixed;
|
||||
|
||||
public function set(string $key, mixed $value): self;
|
||||
|
||||
public function clear(string $key): void;
|
||||
|
||||
public function clearAll(): void;
|
||||
|
||||
public function has(string $key): bool;
|
||||
}
|
||||
@@ -32,4 +32,6 @@ namespace Espo\Core\Templates\Entities;
|
||||
use Espo\Core\Entities\CategoryTreeItem;
|
||||
|
||||
class CategoryTree extends CategoryTreeItem
|
||||
{}
|
||||
{
|
||||
public const string TEMPLATE_TYPE = 'CategoryTree';
|
||||
}
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
"link": "#{entityType}/list",
|
||||
"acl": "read",
|
||||
"style": "default"
|
||||
},
|
||||
{
|
||||
"labelTranslation": "Global.scopeNamesPlural.{subjectEntityType}",
|
||||
"link": "#{subjectEntityType}",
|
||||
"acl": "read",
|
||||
"style": "default",
|
||||
"aclScope": "{subjectEntityType}"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -19,6 +26,13 @@
|
||||
"link": "#{entityType}",
|
||||
"acl": "read",
|
||||
"style": "default"
|
||||
},
|
||||
{
|
||||
"labelTranslation": "Global.scopeNamesPlural.{subjectEntityType}",
|
||||
"link": "#{subjectEntityType}",
|
||||
"acl": "read",
|
||||
"style": "default",
|
||||
"aclScope": "{subjectEntityType}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -29,4 +43,4 @@
|
||||
"unlinkDisabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
{
|
||||
"entity": true,
|
||||
"layouts": true,
|
||||
"tab": true,
|
||||
"tab": false,
|
||||
"acl": true,
|
||||
"aclPortal": true,
|
||||
"customizable": true,
|
||||
"aclLevelList": [
|
||||
"all",
|
||||
"team",
|
||||
"no"
|
||||
],
|
||||
"aclPortalLevelList": [
|
||||
"all",
|
||||
"no"
|
||||
],
|
||||
"importable": false,
|
||||
"notifications": false
|
||||
}
|
||||
"notifications": false,
|
||||
"customizable": false,
|
||||
"entityManager": {
|
||||
"fields": false,
|
||||
"formula": false,
|
||||
"relationships": false,
|
||||
"addField": false,
|
||||
"edit": false,
|
||||
"layouts": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,19 +53,5 @@
|
||||
"forcePatchAttributeDependencyMap": {
|
||||
"dateEnd": ["dateStart"],
|
||||
"dateEndDate": ["dateStartDate"]
|
||||
},
|
||||
"dynamicLogic":{
|
||||
"fields":{
|
||||
"duration": {
|
||||
"readOnly": {
|
||||
"conditionGroup": [
|
||||
{
|
||||
"type": "isTrue",
|
||||
"attribute": "isAllDay"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"fields":{
|
||||
"duration": {
|
||||
"readOnly": {
|
||||
"conditionGroup": [
|
||||
{
|
||||
"type": "isTrue",
|
||||
"attribute": "isAllDay"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create {entityType}": "Create {entityTypeTranslated}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Upgrades\Migrations\V9_4;
|
||||
|
||||
use Espo\Core\Upgrades\Migration\Script;
|
||||
use Espo\Entities\Preferences;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class AfterUpgrade implements Script
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
) {}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->updatePreferences();
|
||||
}
|
||||
|
||||
private function updatePreferences(): void
|
||||
{
|
||||
$users = $this->entityManager
|
||||
->getRDBRepositoryByClass(User::class)
|
||||
->sth()
|
||||
->where([
|
||||
User::ATTR_IS_ACTIVE => true,
|
||||
User::ATTR_TYPE => [
|
||||
User::TYPE_ADMIN,
|
||||
User::TYPE_REGULAR,
|
||||
User::TYPE_PORTAL,
|
||||
]
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$preferences = $this->entityManager->getRepositoryByClass(Preferences::class)->getById($user->getId());
|
||||
|
||||
if (!$preferences) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$preferences->set('notificationGrouping', true);
|
||||
$this->entityManager->saveEntity($preferences);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,11 @@ class RelationConverter
|
||||
{
|
||||
private const DEFAULT_VARCHAR_LENGTH = 255;
|
||||
|
||||
/** @var string[] */
|
||||
/**
|
||||
* Parameters to merge from both sides.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $mergeParams = [
|
||||
RelationParam::RELATION_NAME,
|
||||
RelationParam::CONDITIONS,
|
||||
@@ -118,8 +122,14 @@ class RelationConverter
|
||||
$raw = $convertedEntityDefs->toAssoc();
|
||||
|
||||
if (isset($raw[EntityParam::RELATIONS][$name])) {
|
||||
$this->mergeParams($raw[EntityParam::RELATIONS][$name], $params, $foreignParams ?? [], $linkType);
|
||||
$this->correct($raw[EntityParam::RELATIONS][$name]);
|
||||
$defs = &$raw[EntityParam::RELATIONS][$name];
|
||||
|
||||
$this->mergeParams($defs, $params, $foreignParams ?? [], $linkType);
|
||||
$this->correct($defs);
|
||||
|
||||
if ($params[RelationParam::CASCADE_REMOVAL] ?? false) {
|
||||
$defs[RelationParam::CASCADE_REMOVAL] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return [$entityType => $raw];
|
||||
|
||||
@@ -33,6 +33,7 @@ use Espo\Core\Utils\DataUtil;
|
||||
use Espo\Core\Utils\Metadata\AdditionalBuilder;
|
||||
use Espo\Core\Utils\Metadata\BuilderHelper;
|
||||
use Espo\Core\Utils\Util;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
|
||||
class Fields implements AdditionalBuilder
|
||||
@@ -75,6 +76,10 @@ class Fields implements AdditionalBuilder
|
||||
}
|
||||
|
||||
foreach (get_object_vars($entityDefsItem->fields) as $field => $fieldDefsItem) {
|
||||
if (!is_object($fieldDefsItem)) {
|
||||
throw new RuntimeException("Bad definition for $entityType.$field field.");
|
||||
}
|
||||
|
||||
$additionalFields = $this->builderHelper->getAdditionalFields(
|
||||
field: $field,
|
||||
params: Util::objectToArray($fieldDefsItem),
|
||||
|
||||
@@ -57,6 +57,25 @@ class Builder
|
||||
['app', 'client', 'scriptList'],
|
||||
['app', 'client', 'cssList'],
|
||||
['app', 'client', 'linkList'],
|
||||
|
||||
['app', 'record', 'selectApplierClassNameList'],
|
||||
['app', 'record', 'createInputFilterClassNameList'],
|
||||
['app', 'record', 'updateInputFilterClassNameList'],
|
||||
['app', 'record', 'outputFilterClassNameList'],
|
||||
['app', 'record', 'beforeReadHookClassNameList'],
|
||||
['app', 'record', 'earlyBeforeCreateHookClassNameList'],
|
||||
['app', 'record', 'beforeCreateHookClassNameList'],
|
||||
['app', 'record', 'earlyBeforeUpdateHookClassNameList'],
|
||||
['app', 'record', 'beforeUpdateHookClassNameList'],
|
||||
['app', 'record', 'beforeDeleteHookClassNameList'],
|
||||
['app', 'record', 'afterCreateHookClassNameList'],
|
||||
['app', 'record', 'afterUpdateHookClassNameList'],
|
||||
['app', 'record', 'afterDeleteHookClassNameList'],
|
||||
['app', 'record', 'beforeLinkHookClassNameList'],
|
||||
['app', 'record', 'beforeUnlinkHookClassNameList'],
|
||||
['app', 'record', 'afterLinkHookClassNameList'],
|
||||
['app', 'record', 'afterUnlinkHookClassNameList'],
|
||||
|
||||
['recordDefs', self::ANY_KEY, 'readLoaderClassNameList'],
|
||||
['recordDefs', self::ANY_KEY, 'listLoaderClassNameList'],
|
||||
['recordDefs', self::ANY_KEY, 'saverClassNameList'],
|
||||
|
||||
@@ -578,6 +578,8 @@ class Util
|
||||
*
|
||||
* @param mixed $var1
|
||||
* @param mixed $var2
|
||||
* @deprecated As of v9.4.
|
||||
* @todo Remove in v10.0.
|
||||
*/
|
||||
public static function areEqual($var1, $var2): bool
|
||||
{
|
||||
@@ -596,6 +598,8 @@ class Util
|
||||
* Sort array recursively.
|
||||
*
|
||||
* @param array<string|int, mixed> $array
|
||||
* @deprecated As of v9.4.
|
||||
* @todo Remove in v10.0.
|
||||
*/
|
||||
public static function ksortRecursive(&$array): bool
|
||||
{
|
||||
@@ -786,9 +790,12 @@ class Util
|
||||
|
||||
/**
|
||||
* Check whether values are equal.
|
||||
* Not to be used for comparing user input values.
|
||||
*
|
||||
* @param mixed $v1
|
||||
* @param mixed $v2
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function areValuesEqual($v1, $v2, bool $isUnordered = false): bool
|
||||
{
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Core\Webhook;
|
||||
|
||||
use Espo\Core\HttpClient\Util;
|
||||
use Espo\Core\Utils\Config;
|
||||
|
||||
/**
|
||||
@@ -48,32 +49,6 @@ class AddressUtil
|
||||
/** @var string[] $allowedAddressList */
|
||||
$allowedAddressList = $this->config->get('webhookAllowedAddressList') ?? [];
|
||||
|
||||
if (!$allowedAddressList) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
$port = parse_url($url, PHP_URL_PORT);
|
||||
$scheme = parse_url($url, PHP_URL_SCHEME);
|
||||
|
||||
if (!is_string($host)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_int($port)) {
|
||||
if ($scheme === 'https') {
|
||||
$port = 443;
|
||||
} else if ($scheme === 'http') {
|
||||
$port = 80;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_int($port)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$address = $host . ':' . $port;
|
||||
|
||||
return in_array($address, $allowedAddressList);
|
||||
return Util::matchUrlToAddressList($url, $allowedAddressList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +283,11 @@ class Queue
|
||||
} catch (Exception $e) {
|
||||
$this->failQueueItemList($itemList, true);
|
||||
|
||||
$this->log->error("Webhook Queue: Webhook '{$webhook->getId()}' sending failed. Error: {$e->getMessage()}");
|
||||
$this->log->error("Webhook Queue: Webhook '{id}' sending failed; {message}", [
|
||||
'exception' => $e,
|
||||
'id' => $webhook->getId(),
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -305,7 +309,10 @@ class Queue
|
||||
|
||||
protected function logSending(Webhook $webhook, int $code): void
|
||||
{
|
||||
$this->log->debug("Webhook Queue: Webhook '{$webhook->getId()}' sent, response code: $code.");
|
||||
$this->log->debug("Webhook Queue: Webhook '{id}' sent, response code: {code}.", [
|
||||
'id' => $webhook->getId(),
|
||||
'code' => $code,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,19 +34,23 @@ use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Core\Utils\Security\UrlCheck;
|
||||
use Espo\Entities\Webhook;
|
||||
use Espo\Core\HttpClient;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use LogicException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* Sends a portion.
|
||||
*/
|
||||
class Sender
|
||||
{
|
||||
private const CONNECT_TIMEOUT = 5;
|
||||
private const TIMEOUT = 10;
|
||||
private const int CONNECT_TIMEOUT = 5;
|
||||
private const int TIMEOUT = 10;
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private UrlCheck $urlCheck,
|
||||
private AddressUtil $addressUtil,
|
||||
private HttpClient\ClientFactory $clientFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -57,91 +61,44 @@ class Sender
|
||||
{
|
||||
$payload = Json::encode($dataList);
|
||||
|
||||
$signature = null;
|
||||
$legacySignature = null;
|
||||
[$signature, $legacySignature] = $this->prepareSignatures($webhook, $payload);
|
||||
|
||||
$secretKey = $webhook->getSecretKey();
|
||||
$options = new HttpClient\Options(
|
||||
protocols: [HttpClient\Protocol::https, HttpClient\Protocol::http],
|
||||
redirect: new HttpClient\Options\Redirect(
|
||||
allow: true,
|
||||
protocols: [HttpClient\Protocol::https],
|
||||
),
|
||||
timeout: $this->getTimeout(),
|
||||
connectTimeout: $this->getConnectTimeout(),
|
||||
internalHostRestriction: new HttpClient\Options\InternalHostRestriction(
|
||||
restrict: true,
|
||||
allowed: $this->getAllowedAddressList(),
|
||||
),
|
||||
);
|
||||
|
||||
if ($secretKey) {
|
||||
$signature = $this->buildSignature($webhook, $payload, $secretKey);
|
||||
$legacySignature = $this->buildSignatureLegacy($webhook, $payload, $secretKey);
|
||||
$request = $this->prepareRequest(
|
||||
url: $this->getUrl($webhook),
|
||||
payload: $payload,
|
||||
signature: $signature,
|
||||
legacySignature: $legacySignature,
|
||||
);
|
||||
|
||||
$client = $this->clientFactory->create($options);
|
||||
|
||||
try {
|
||||
$response = $client->send($request);
|
||||
} catch (HttpClient\Exceptions\ConnectException $e) {
|
||||
if ($e->getReason() === HttpClient\ConnectErrorReason::Timeout) {
|
||||
return 408;
|
||||
}
|
||||
|
||||
throw new Error("Connect error.", previous: $e);
|
||||
} catch (HttpClient\Exceptions\TooManyRedirectsException $e) {
|
||||
throw new Error("Too many redirects.", previous: $e);
|
||||
}
|
||||
|
||||
$connectTimeout = $this->config->get('webhookConnectTimeout', self::CONNECT_TIMEOUT);
|
||||
$timeout = $this->config->get('webhookTimeout', self::TIMEOUT);
|
||||
|
||||
$headerList = [];
|
||||
|
||||
$headerList[] = 'Content-Type: application/json';
|
||||
$headerList[] = 'Content-Length: ' . strlen($payload);
|
||||
|
||||
if ($signature) {
|
||||
$headerList[] = 'Signature: ' . $signature;
|
||||
}
|
||||
|
||||
if ($legacySignature) {
|
||||
$headerList[] = 'X-Signature: ' . $legacySignature;
|
||||
}
|
||||
|
||||
$url = $webhook->getUrl();
|
||||
|
||||
if (!$url) {
|
||||
throw new Error("Webhook does not have URL.");
|
||||
}
|
||||
|
||||
if (!$this->urlCheck->isUrl($url)) {
|
||||
throw new Error("'$url' is not valid URL.");
|
||||
}
|
||||
|
||||
if (
|
||||
!$this->addressUtil->isAllowedUrl($url) &&
|
||||
!$this->urlCheck->isNotInternalUrl($url)
|
||||
) {
|
||||
throw new Error("URL '$url' points to an internal host, not allowed.");
|
||||
}
|
||||
|
||||
$handler = curl_init($url);
|
||||
|
||||
if ($handler === false) {
|
||||
throw new Error("Could not init CURL for URL {$url}.");
|
||||
}
|
||||
|
||||
curl_setopt($handler, \CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($handler, \CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($handler, \CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($handler, \CURLOPT_HEADER, true);
|
||||
curl_setopt($handler, \CURLOPT_CUSTOMREQUEST, 'POST');
|
||||
curl_setopt($handler, \CURLOPT_CONNECTTIMEOUT, $connectTimeout);
|
||||
curl_setopt($handler, \CURLOPT_TIMEOUT, $timeout);
|
||||
curl_setopt($handler, \CURLOPT_PROTOCOLS, \CURLPROTO_HTTPS | \CURLPROTO_HTTP);
|
||||
curl_setopt($handler, \CURLOPT_REDIR_PROTOCOLS, \CURLPROTO_HTTPS);
|
||||
curl_setopt($handler, \CURLOPT_HTTPHEADER, $headerList);
|
||||
curl_setopt($handler, \CURLOPT_POSTFIELDS, $payload);
|
||||
|
||||
curl_exec($handler);
|
||||
|
||||
$code = curl_getinfo($handler, \CURLINFO_HTTP_CODE);
|
||||
|
||||
if (!is_numeric($code)) {
|
||||
$code = 0;
|
||||
}
|
||||
|
||||
if (!is_int($code)) {
|
||||
$code = intval($code);
|
||||
}
|
||||
|
||||
$errorNumber = curl_errno($handler);
|
||||
|
||||
if (
|
||||
$errorNumber &&
|
||||
in_array($errorNumber, [\CURLE_OPERATION_TIMEDOUT, \CURLE_OPERATION_TIMEOUTED])
|
||||
) {
|
||||
$code = 408;
|
||||
}
|
||||
|
||||
curl_close($handler);
|
||||
|
||||
return $code;
|
||||
return $response->getStatusCode();
|
||||
}
|
||||
|
||||
private function buildSignature(Webhook $webhook, string $payload, string $secretKey): string
|
||||
@@ -159,4 +116,86 @@ class Sender
|
||||
{
|
||||
return base64_encode($webhook->getId() . ':' . hash_hmac('sha256', $payload, $secretKey, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @throws Error
|
||||
*/
|
||||
private function getUrl(Webhook $webhook): string
|
||||
{
|
||||
$url = $webhook->getUrl() ?? throw new Error("Webhook does not have URL.");
|
||||
|
||||
if (!$this->urlCheck->isUrl($url)) {
|
||||
throw new Error("'$url' is not valid URL.");
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getAllowedAddressList(): array
|
||||
{
|
||||
/** @var string[] $allowedAddressList */
|
||||
$allowedAddressList = $this->config->get('webhookAllowedAddressList') ?? [];
|
||||
|
||||
return $allowedAddressList;
|
||||
}
|
||||
|
||||
private function prepareRequest(
|
||||
string $url,
|
||||
string $payload,
|
||||
?string $signature,
|
||||
?string $legacySignature,
|
||||
): RequestInterface {
|
||||
|
||||
$request = (new Request('POST', $url))
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withHeader('Content-Length', (string) strlen($payload));
|
||||
|
||||
if ($signature) {
|
||||
$request = $request->withHeader('Signature', $signature);
|
||||
}
|
||||
|
||||
if ($legacySignature) {
|
||||
$request = $request->withHeader('X-Signature', $legacySignature);
|
||||
}
|
||||
|
||||
$request = $request->withBody(HttpClient\Util::streamFor($payload));
|
||||
|
||||
if (!$request instanceof RequestInterface) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: ?string, 1: ?string}
|
||||
*/
|
||||
private function prepareSignatures(Webhook $webhook, string $payload): array
|
||||
{
|
||||
$signature = null;
|
||||
$legacySignature = null;
|
||||
|
||||
$secretKey = $webhook->getSecretKey();
|
||||
|
||||
if ($secretKey) {
|
||||
$signature = $this->buildSignature($webhook, $payload, $secretKey);
|
||||
$legacySignature = $this->buildSignatureLegacy($webhook, $payload, $secretKey);
|
||||
}
|
||||
|
||||
return [$signature, $legacySignature];
|
||||
}
|
||||
|
||||
private function getConnectTimeout(): ?int
|
||||
{
|
||||
return $this->config->get('webhookConnectTimeout', self::CONNECT_TIMEOUT);
|
||||
}
|
||||
|
||||
private function getTimeout(): ?int
|
||||
{
|
||||
return $this->config->get('webhookTimeout', self::TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,16 @@ class Note extends Entity
|
||||
public const TYPE_EMAIL_RECEIVED = 'EmailReceived';
|
||||
public const TYPE_EMAIL_SENT = 'EmailSent';
|
||||
|
||||
public const string DATA_ATTR_ADDED_ASSIGNED_USERS = 'addedAssignedUsers';
|
||||
public const string DATA_ATTR_REMOVED_ASSIGNED_USERS = 'removedAssignedUsers';
|
||||
public const string DATA_ATTR_ASSIGNED_USERS = 'assignedUsers';
|
||||
public const string DATA_ATTR_STATUS_VALUE = 'statusValue';
|
||||
public const string DATA_ATTR_STATUS_FIELD = 'statusField';
|
||||
public const string DATA_ATTR_ASSIGNED_USER_ID = 'assignedUserId';
|
||||
public const string DATA_ATTR_ASSIGNED_USER_NAME = 'assignedUserName';
|
||||
public const string DATA_ATTR_FIELDS = 'fields';
|
||||
public const string DATA_ATTR_ATTRIBUTES = 'attributes';
|
||||
|
||||
private bool $aclIsProcessed = false;
|
||||
|
||||
public function isPost(): bool
|
||||
@@ -341,6 +351,16 @@ class Note extends Entity
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 9.4.0
|
||||
*/
|
||||
public function setIsInternal(bool $isInternal): self
|
||||
{
|
||||
$this->set('isInternal', $isInternal);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParent(): ?OrmEntity
|
||||
{
|
||||
return $this->relations->getOne(Field::PARENT);
|
||||
|
||||
@@ -54,6 +54,22 @@ class Notification extends Entity
|
||||
public const ATTR_ACTION_ID = 'actionId';
|
||||
public const ATTR_NUMBER = 'number';
|
||||
|
||||
public const string ATTR_RELATED_TYPE = 'relatedType';
|
||||
public const string ATTR_RELATED_ID = 'relatedId';
|
||||
public const string ATTR_RELATED_PARENT_TYPE = 'relatedParentType';
|
||||
public const string ATTR_RELATED_PARENT_ID = 'relatedParentId';
|
||||
|
||||
public const string FIELD_DATA = 'data';
|
||||
public const string FIELD_MESSAGE = 'message';
|
||||
public const string FIELD_TYPE = 'type';
|
||||
public const string FIELD_RELATED_PARENT = 'relatedParent';
|
||||
public const string FIELD_IS_FEATURED = 'isFeatured';
|
||||
|
||||
public const string GROUP_TYPE_NOTE = Notification::TYPE_NOTE;
|
||||
public const string GROUP_TYPE_EMAIL_RECEIVED = Notification::TYPE_EMAIL_RECEIVED;
|
||||
|
||||
public const string DATE_ATTR_NOTE_ID = 'noteId';
|
||||
|
||||
public function getType(): ?string
|
||||
{
|
||||
return $this->get('type');
|
||||
@@ -128,6 +144,12 @@ class Notification extends Entity
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRelatedParent(): ?LinkParent
|
||||
{
|
||||
/** @var ?LinkParent */
|
||||
return $this->getValueObject('relatedParent');
|
||||
}
|
||||
|
||||
public function setRelatedParent(LinkParent|Entity|null $relatedParent): self
|
||||
{
|
||||
if ($relatedParent instanceof LinkParent) {
|
||||
@@ -185,8 +207,27 @@ class Notification extends Entity
|
||||
return $this->get('actionId');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setGroupedCount(?int $groupedCount): self
|
||||
{
|
||||
return $this->set('groupedCount', $groupedCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getGroupType(): ?string
|
||||
{
|
||||
return $this->get('groupType');
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 9.4.0
|
||||
*/
|
||||
public function setIsFeatured(bool $isFeatured): self
|
||||
{
|
||||
return $this->set(self::FIELD_IS_FEATURED, $isFeatured);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
namespace Espo\Hooks\Common;
|
||||
|
||||
use Espo\Core\Hook\Hook\AfterSave;
|
||||
use Espo\Core\ORM\Repository\Option\SaveContext;
|
||||
use Espo\ORM\Repository\Option\SaveContext;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Repository\Option\SaveOptions;
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Hooks\Common;
|
||||
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Hook\Hook\BeforeRemove;
|
||||
use Espo\Core\Hook\Hook\BeforeSave;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Exceptions\ValidationException;
|
||||
use Espo\ORM\Repository\Option\RemoveOptions;
|
||||
use Espo\ORM\Repository\Option\SaveOptions;
|
||||
use Espo\Tools\Lock\LockValidationHelper;
|
||||
use Espo\Tools\Lock\LockMetadataProvider;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class ValidateLocked implements BeforeSave, BeforeRemove
|
||||
{
|
||||
public static int $order = 12;
|
||||
|
||||
public function __construct(
|
||||
private LockMetadataProvider $lockMetadataProvider,
|
||||
private LockValidationHelper $lockValidationHelper,
|
||||
) {}
|
||||
|
||||
public function beforeSave(Entity $entity, SaveOptions $options): void
|
||||
{
|
||||
if (!$this->lockMetadataProvider->isEnabled($entity->getEntityType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->lockValidationHelper->validateBeforeSave($entity);
|
||||
} catch (Conflict $e) {
|
||||
throw new ValidationException(previous: $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function beforeRemove(Entity $entity, RemoveOptions $options): void
|
||||
{
|
||||
if (!$this->lockMetadataProvider->isEnabled($entity->getEntityType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->lockValidationHelper->validateBeforeRemove($entity);
|
||||
} catch (Conflict $e) {
|
||||
throw new ValidationException(previous: $e);
|
||||
}
|
||||
|
||||
if ($entity->get(Field::IS_LOCKED)) {
|
||||
throw new ValidationException("Cannot remove a locked record.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
namespace Espo\Hooks\Note;
|
||||
|
||||
use Espo\Core\Hook\Hook\AfterSave;
|
||||
use Espo\Core\ORM\Repository\Option\SaveContext;
|
||||
use Espo\ORM\Repository\Option\SaveContext;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Repository\Option\SaveOptions;
|
||||
use Espo\Tools\Notification\HookProcessor\Params;
|
||||
|
||||
@@ -151,7 +151,7 @@ class Invitations
|
||||
->setSubject($subject)
|
||||
->setBody($body)
|
||||
->setIsHtml()
|
||||
->setParent(LinkParent::createFromEntity($entity));
|
||||
->setParent(LinkParent::fromEntity($entity));
|
||||
|
||||
$attachmentName = ucwords($this->language->translateLabel($entity->getEntityType(), 'scopeNames')) . '.ics';
|
||||
|
||||
@@ -195,7 +195,7 @@ class Invitations
|
||||
$terminateAt = $dt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT);
|
||||
}
|
||||
|
||||
$uid->setTarget(LinkParent::createFromEntity($entity));
|
||||
$uid->setTarget(LinkParent::fromEntity($entity));
|
||||
$uid->setTerminateAt(DateTimeField::fromString($terminateAt));
|
||||
|
||||
$this->entityManager->saveEntity($uid);
|
||||
|
||||
@@ -114,7 +114,7 @@ class Meeting implements AssignmentNotificator
|
||||
$notification
|
||||
->setType(self::NOTIFICATION_TYPE_EVENT_ATTENDEE)
|
||||
->setUserId($userId)
|
||||
->setRelated(LinkParent::createFromEntity($entity))
|
||||
->setRelated(LinkParent::fromEntity($entity))
|
||||
->setData([
|
||||
'entityType' => $entity->getEntityType(),
|
||||
'entityId' => $entity->getId(),
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Crm\Classes\FieldValidators\Meeting\ExternalService;
|
||||
|
||||
use Espo\Core\FieldValidation\Validator;
|
||||
use Espo\Core\FieldValidation\Validator\Data;
|
||||
use Espo\Core\FieldValidation\Validator\Failure;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Modules\Crm\Entities\Meeting;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @implements Validator<Meeting>
|
||||
*/
|
||||
class Valid implements Validator
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
) {}
|
||||
|
||||
public function validate(Entity $entity, string $field, Data $data): ?Failure
|
||||
{
|
||||
$service = $entity->getExternalService();
|
||||
|
||||
if (!$service) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->metadata->get("app.meetingServices.$service.enabled")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Failure::create();
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Modules\Crm\Classes\RecordHooks\Meeting;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Record\Hook\SaveHook;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Crm\Entities\Meeting;
|
||||
use Espo\Modules\Crm\Tools\Meeting\MeetingServiceAvailabilityCheckerFactory;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* @implements SaveHook<Meeting>
|
||||
*/
|
||||
class BeforeCreateExternalServiceCheck implements SaveHook
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private MeetingServiceAvailabilityCheckerFactory $factory,
|
||||
private User $user,
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity): void
|
||||
{
|
||||
$service = $entity->getExternalService();
|
||||
|
||||
if (!$service) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->metadata->get("app.meetingServices.$service.enabled")) {
|
||||
throw new BadRequest("Not supported service '$service'.");
|
||||
}
|
||||
|
||||
$checker = $this->factory->create($service);
|
||||
|
||||
if (!$checker->check($this->user)) {
|
||||
throw new Forbidden("Not allowed service '$service'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,16 @@ class Contact extends Person
|
||||
{
|
||||
public const ENTITY_TYPE = 'Contact';
|
||||
|
||||
/** @since v9.3.0. */
|
||||
public const string ATTR_ACCOUNT_ID = 'accountId';
|
||||
/** @since v9.3.0. */
|
||||
public const string FIELD_ACCOUNTS = 'accounts';
|
||||
/** @since v9.3.0. */
|
||||
public const string FIELD_ACCOUNT = 'account';
|
||||
|
||||
/** @since v9.3.0. */
|
||||
public const string RELATIONSHIP_ACCOUNT_CONTACT = 'AccountContact';
|
||||
|
||||
/** @since v9.3.0. */
|
||||
public const string COLUMN_ACCOUNTS_ROLE = 'role';
|
||||
|
||||
@@ -61,6 +71,27 @@ class Contact extends Person
|
||||
return $this->relations->getOne('account');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get accounts as link-multiple.
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
public function getAccountsLinkMultiple(): LinkMultiple
|
||||
{
|
||||
/** @var LinkMultiple */
|
||||
return $this->getValueObject('accounts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set accounts.
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
public function setAccounts(LinkMultiple $accounts): self
|
||||
{
|
||||
return $this->setValueObject(self::FIELD_ACCOUNTS, $accounts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get accounts.
|
||||
*
|
||||
|
||||
@@ -247,4 +247,12 @@ class Meeting extends Entity
|
||||
{
|
||||
return $this->get('joinUrl');
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 9.4.0
|
||||
*/
|
||||
public function getExternalService(): ?string
|
||||
{
|
||||
return $this->get('externalService');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,36 +29,49 @@
|
||||
|
||||
namespace Espo\Modules\Crm\Hooks\Contact;
|
||||
|
||||
use Espo\Core\Field\Link;
|
||||
use Espo\Core\Field\LinkMultipleItem;
|
||||
use Espo\Core\Hook\Hook\AfterRelate;
|
||||
use Espo\Core\Hook\Hook\AfterSave;
|
||||
use Espo\Core\Hook\Hook\BeforeSave;
|
||||
use Espo\Modules\Crm\Entities\Contact;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
use Espo\ORM\Repository\Option\RelateOptions;
|
||||
use Espo\ORM\Repository\Option\SaveOptions;
|
||||
|
||||
/**
|
||||
* @implements BeforeSave<Contact>
|
||||
* @implements AfterSave<Contact>
|
||||
* @implements AfterRelate<Contact>
|
||||
*/
|
||||
class Accounts implements AfterSave
|
||||
class Accounts implements BeforeSave, AfterSave, AfterRelate
|
||||
{
|
||||
private const string COLUMN_ROLE = Contact::COLUMN_ACCOUNTS_ROLE;
|
||||
private const string ATTR_TITLE = 'title';
|
||||
|
||||
public function __construct(private EntityManager $entityManager) {}
|
||||
|
||||
/**
|
||||
* @param Contact $entity
|
||||
*/
|
||||
public function beforeSave(Entity $entity, SaveOptions $options): void
|
||||
{
|
||||
$this->setPrimary($entity);
|
||||
$this->restoreAccounts($entity);
|
||||
}
|
||||
|
||||
public function afterSave(Entity $entity, SaveOptions $options): void
|
||||
{
|
||||
$accountIdChanged = $entity->isAttributeChanged('accountId');
|
||||
$titleChanged = $entity->isAttributeChanged('title');
|
||||
$accountIdChanged = $entity->isAttributeChanged(Contact::ATTR_ACCOUNT_ID);
|
||||
$titleChanged = $entity->isAttributeChanged(self::ATTR_TITLE);
|
||||
|
||||
/** @var ?string $fetchedAccountId */
|
||||
$fetchedAccountId = $entity->getFetched('accountId');
|
||||
$fetchedAccountId = $entity->getFetched(Contact::ATTR_ACCOUNT_ID);
|
||||
$accountId = $entity->getAccount()?->getId();
|
||||
$title = $entity->getTitle();
|
||||
|
||||
$relation = $this->entityManager
|
||||
->getRDBRepositoryByClass(Contact::class)
|
||||
->getRelation($entity, 'accounts');
|
||||
->getRelation($entity, Contact::FIELD_ACCOUNTS);
|
||||
|
||||
if (!$accountId && $fetchedAccountId) {
|
||||
$relation->unrelateById($fetchedAccountId);
|
||||
@@ -75,8 +88,8 @@ class Accounts implements AfterSave
|
||||
}
|
||||
|
||||
$accountContact = $this->entityManager
|
||||
->getRDBRepository('AccountContact')
|
||||
->select(['role'])
|
||||
->getRDBRepository(Contact::RELATIONSHIP_ACCOUNT_CONTACT)
|
||||
->select([self::COLUMN_ROLE])
|
||||
->where([
|
||||
'accountId' => $accountId,
|
||||
'contactId' => $entity->getId(),
|
||||
@@ -85,13 +98,79 @@ class Accounts implements AfterSave
|
||||
->findOne();
|
||||
|
||||
if (!$accountContact && $accountIdChanged) {
|
||||
$relation->relateById($accountId, ['role' => $title]);
|
||||
$relation->relateById($accountId, [self::COLUMN_ROLE => $title]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($titleChanged && $accountContact && $title !== $accountContact->get('role')) {
|
||||
$relation->updateColumnsById($accountId, ['role' => $title]);
|
||||
if ($titleChanged && $accountContact && $title !== $accountContact->get(self::COLUMN_ROLE)) {
|
||||
$relation->updateColumnsById($accountId, [self::COLUMN_ROLE => $title]);
|
||||
}
|
||||
}
|
||||
|
||||
private function setPrimary(Contact $entity): void
|
||||
{
|
||||
if (
|
||||
!$entity->isAttributeChanged(Contact::ATTR_ACCOUNT_ID) &&
|
||||
!$entity->isAttributeChanged(Contact::FIELD_ACCOUNTS . 'Ids')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$entity->getAccount() && $entity->getAccountsLinkMultiple()->getList()) {
|
||||
$first = $entity->getAccountsLinkMultiple()->getList()[0];
|
||||
|
||||
$entity->setAccount(Link::create($first->getId(), $first->getName()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the form is saved but there is no the accounts, the columns is still sent. Restore the accounts then.
|
||||
*/
|
||||
private function restoreAccounts(Contact $entity): void
|
||||
{
|
||||
$account = $entity->getAccount();
|
||||
|
||||
if (!$account || !$entity->isAttributeChanged(Contact::FIELD_ACCOUNTS . 'Columns')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$accounts = $entity->getAccountsLinkMultiple();
|
||||
|
||||
if ($accounts->hasId($account->getId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$accounts = $accounts->withAdded(
|
||||
LinkMultipleItem::fromEntity($account)->withName($account->getName())
|
||||
->withColumnValue(self::COLUMN_ROLE, null)
|
||||
);
|
||||
|
||||
$entity->setAccounts($accounts);
|
||||
}
|
||||
|
||||
public function afterRelate(
|
||||
Entity $entity,
|
||||
string $relationName,
|
||||
Entity $relatedEntity,
|
||||
array $columnData,
|
||||
RelateOptions $options,
|
||||
): void {
|
||||
|
||||
if ($relationName !== Contact::FIELD_ACCOUNTS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($entity->getAccount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$relation = $this->entityManager->getRelation($entity, Contact::FIELD_ACCOUNT);
|
||||
|
||||
if ($relation->findOne()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$relation->relate($relatedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user